The Timing Object - A Pacemaker for the Web

The Timing Object specification is aiming to be a new standard which drastically simplifies synchronizing timed content on the web. It has been around for about 4 years now but is still not yet implemented in any browser. However this is no reason to wait before using it. Many of its features can be implemented in user land.
This post aims to give you a quick overview about the Timing Object. It also shows what can already be done today without waiting for the browser vendors to catch up.

I gave a talk at this years Web Audio Conference covering almost the same content. The recording is available on YouTube if you prefer watching a video instead. The slides of my presentation are available online as well.

What is the Timing Object (not)?

The Timing Object is a W3C draft which is authored by the Multi Device Timing Community Group. Being a draft means that it is not even on a standards track yet. None of the big browser vendors jumped on it and as a result there is no native implementation in any browser so far. But in contrast to many other new and exciting APIs there is not much that needs to be implemented in the browser for the Timing Object to work. In fact almost all of its functionality can be used right now.

I think it is important to note that the Timing Object is not another mechanism to defer the execution of some code. It is not meant to replace setTimeout() or requestAnimationFrame(). And it does also not help with the automation of an AudioParam. It is more the other way round that the Timing Object relies on the before mentioned scheduling APIs on the inside. The Timing Object does a great job at controlling time sensitive content on an abstract level and provides high interoperability between different parts of one or even multiple websites.

What can it be used for?

A little story might be helpful to highlight where the Timing Object shines:

Let's imagine it is the final day of the World Cup. You've been desperatly waiting for it for four years. And now the time has finally come. It is quarter to nine and you are sitting in front of your laptop, smart-tv or whatever it is you use to watch live sports these days.

Five minutes in the game you suddenly realize that you forgot to hand in a proposal which is due to be submitted until the next day. Not every conference is organized by friendly people which happily extend the deadline and so you pause the live stream to proof read your proposal one more time before you finally upload it.

It took you about 30 minutes and once you're done you resume the live stream and continue watching the game from where you left off. That's not an issue because you live in the middle of nowhere and there is no chance that you will hear any cheering from your neighbours. Nothing can happen which would disrupt the illusion of watching the game live even though you are actually 30 minutes behind in time.

A couple of minutes later one team scores a goal but soon after that short burst of excitement the game is getting a little boring. Obviously you don't want to skip ahead since you are afraid of missing something. Luckily the broadcasting company is also offering a huge collection of live stats which you could view while you are waiting for the tension to rise again. You pull out your smartphone, open the web app for the stats and start screaming right away.

What went wrong? The first stat the web app shows is the number goals. This is obviously an important statistic but unfortunately it reveals that one team is leading the game 2 to 1.

It looks like the stats app is 30 minutes ahaid of you in time. It doesn't know that you timeshifted the video stream. It reveals the result at halftime. So there is no point in watching the rest of the first half anymore. You already know what is going to happen. You just skip ahead and continue watching the game's second half in real-time again.

This is of course a totally undesirable user experience. It would have been much better to sync the live stream and the stats app. The truth is that the broadcasting company could have implemented the synchronization with exsiting APIs. But it is complicated and this is probably the reason why it is often not done at all. The Timing Object makes it super simple to implement something like this and the rest of the post hopefully shows how that works.

How does it work?

The core concept of the Timing Object specification is the Timing­State­Vector. It is the source of truth. There can only be one Timing­State­Vector at a given time. It looks like this when defined as a TypeScript interface:

interface TimingStateVector {

  position: number;
  velocity: number;
  acceleration: number;

  timestamp: number;

}

The position is an arbitrary number which depends on the thing the Timing­State­Vector refers to. In the context of a video player this would most likely represent the position in seconds. If the video is playing at regular speed the velocity will be 1 and the acceleration will be 0. The timestamp is the point in time at which this Timing­State­Vector was valid. In combination with the velocity and acceleration the position for any other point in time can easily be computed.

How can it be used?

TimingObject is not only the name of the specification it is also the name of a class wrapped around the Timing­State­Vector. It has basically two methods for querying and updating the currently valid Timing­State­Vector.

interface TimingStateVector {

  query (): TimingStateVector;

  update (
    vector: TimingStateVectorUpdate
  ): Promise<void>;

  // ...

}

The method to query the current Timing­State­Vector is cheap and can be called often. It returns a synchronous result.

The counterpart is a method to update the Timing­State­Vector. It is potentially costly and can even fail. It works asynchronously and is expected not to be called as often as the query() function.

The idea is that you can use a TimingObject to synchronize two (or more) Web Components. To implement the example mentioned in the story above we might have two Web Components which we use to compose the final website.

<live-stream></live-stream>
<game-stats></game-stats>

In order to synchronize these two components we need a TimingObject which then gets assigned to the timingSrc property of both components.

const timingObject = new TimingObject();

querySelector('live-stream').timingSrc = timingObject;
querySelector('game-stats').timingSrc = timingObject;

This magic timingSrc property is expected to come to native elements like the HTMLMediaElement. And this is actually one of the few things which cannot be implemented without the help of the browser vendors. But as shown here it can also be part of the API of any custom Web Component.

How does it really work?

There is one missing piece needed to implement the experience mentioned in the story above. This missing piece is the TimingProvider. It can be thought of as a higher level Timing Object. It's job is to synchronize various TimingObjects across the barrier of a browser context.

The TimingProvider is not meant to be implemented by any browser vendor. It should be implemented by third party vendors "to leave space open for innovation", as it is written in the spec. There is a reference implementation by the Motion Corporation. It uses a central server and connects all clients with a WebSocket connection under the hood. I implemented an alternative version which uses WebRTC to synchronize the clients. My motivation for doing this was to achieve faster synchronization for clients which are on the same LAN.

No matter what TimingProvider you choose, the usage looks like this:

import { TimingProvider } from 'any-timing-provider';

const timingProvider = new TimingProvider(/* ... */);
const timingObject = new TimingObject(timingProvider);

The constructor arguments depend on the specific implementation you use. But other than that the interface of the TimingProvider is standardized. Once you have a TimingProvider you can pass it on to a TimingObject. From that point on the TimingObject gets remote controlled by the TimingProvider. Any call to update() on the TimingObject gets forwarded to the TimingProvider as well.

I hope this post motivated you to add a timingSrc property to the next Web Component or library you build. Or even better, what about opening a pull request to implement the timingSrc property for your favorite media player? Wouldn't it be awesome if we end up with a rich ecosystem of synchronizable Web Components which could be sticked together with ease?

Many thanks to Ingar M. Arntzen for providing valuable feedback which greatly helped to shape this article.