Sync JS time between multiple devices
Asked Answered
T

5

27

I'm using the wonderful reveal.js library to create a HTML slideshow. My only problem is that I need it to synchronise across multiple devices.

At the moment I am making a AJAX request to the the time from the server and keep an internal clock for the page.

function syncTime() {
    // Set up our time object, synced by the HTTP DATE header
    // Fetch the page over JS to get just the headers
    console.log("syncing time")
    var r = new XMLHttpRequest();
    r.open('HEAD', document.location, false);
    r.send(null);
    var timestring = r.getResponseHeader("DATE");

    systemtime = new Date(timestring); // Set the time to the date sent from the server
}

Whilst this gets me within 1 or so seconds of accuracy, I need to do better. The difference is really noticeable when the slideshow is auto advancing.

The code is going to be running all on the same platform, so cross-browser compatibility is nothing to worry about.

Here's what I've managed to put together

Any ideas?

Tavia answered 14/5, 2012 at 15:2 Comment(0)
I
21

How about a different approach: who cares about time? (You're not going to reliably sync the system clock with JavaScript.)

Instead, use a Node server with socket.io to synchronize when your clients advance the slideshow. Instead of the clients deciding when to advance, the server tells them to.

This approach comes with the added bonus of being able to manually fiddle with the slideshow while it's running. In the example that follows, I've added a Next button that causes all connected clients to immediately advance to the next slide.

app.js

var express = require('express')
    , app = express.createServer()
    , io = require('socket.io').listen(app)
    , doT = require('dot')
    , slide = 0
    , slides = [
        'http://placekitten.com/700/400?image=13',
        'http://placekitten.com/700/400?image=14',
        'http://placekitten.com/700/400?image=15',
        'http://placekitten.com/700/400?image=16',
        'http://placekitten.com/700/400?image=1',
        'http://placekitten.com/700/400?image=2',
        'http://placekitten.com/700/400?image=3',
        'http://placekitten.com/700/400?image=4',
        'http://placekitten.com/700/400?image=5',
        'http://placekitten.com/700/400?image=6',
        'http://placekitten.com/700/400?image=7',
        'http://placekitten.com/700/400?image=8',
        'http://placekitten.com/700/400?image=9',
        'http://placekitten.com/700/400?image=10',
        'http://placekitten.com/700/400?image=11',
        'http://placekitten.com/700/400?image=12',
    ];

app.listen(70); // listen on port 70

app.register('.html', doT); // use doT to render templates
app.set('view options', {layout:false}); // keep it simple
doT.templateSettings.strip=false; // don't strip line endings from template file

app.get('/', function(req, res) {
    res.render('index.html', { slide: slide, slides: slides });
});

app.post('/next', function(req, res) {
    next();
    res.send(204); // No Content
});

setInterval(next, 4000); // advance slides every 4 seconds

function next() {
    if (++slide >= slides.length) slide = 0;
    io.sockets.emit('slide', slide);
}

views/index.html

This file is processed as a doT template.

<!DOCTYPE html>
<html>
<head>
<title>Synchronized Slideshow</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
var curslide = {{=it.slide}}; // the slide the server is currently on.

$(function() {
    $('#slide' + curslide).css('left',0);

    $('#next').click(function() {
        $.post('/next');
    });
});

var socket = io.connect('http://localhost:70');
socket.on('slide', function(slide) {
    $('#slide' + curslide).animate({left:-700}, 400);
    $('#slide' + slide).css('left',700).show().animate({left:0}, 400);
    curslide = slide;
});
</script>
<style>
#slideshow, .slide { width:700px; height:400px; overflow:hidden; position:relative; }
.slide { position:absolute; top:0px; left:700px; }
</style>
</head>
<body>
    <div id="slideshow">
        {{~it.slides :url:i}}
            <div id="slide{{=i}}" class="slide"><img src="{{=url}}"></div>
        {{~}}
    </div>
    <button id="next">Next &gt;</button>
</body>
</html>

Copy these two files into a folder, then run

$ npm install express socket.io dot
$ node app.js

and navigate to http://localhost:70 in several different windows, then see the magic.

Itinerary answered 19/5, 2012 at 4:16 Comment(3)
Love this answer! Ignoring my question to get to the same objective. I'm going to take this approach and see what I can get out of it!Tavia
I got a couple of errors running this with the latest version of Express. Only when I pinned it to version 2.5.10 and reinstalled did it work. Also, I had to run sudo node app.jsWallaroo
Yeah, this is very old and express obviously has many breaking changes between v2 and v4. The need for sudo comes from listening on port 70, change that to something >1024 to fix.Itinerary
H
26

Measure the elapsed time between sending the request and getting back the response. Then, divide that value by 2. That gives you a rough value of one-way latency. If you add that to the time value from the server, you'll be closer to the true server time.

Something like this should work:

function syncTime() {
    // Set up our time object, synced by the HTTP DATE header
    // Fetch the page over JS to get just the headers
    console.log("syncing time")
    var r = new XMLHttpRequest();
    var start = (new Date).getTime();

    r.open('HEAD', document.location, false);
    r.onreadystatechange = function()
    {
        if (r.readyState != 4)
        {
            return;
        }
        var latency = (new Date).getTime() - start;
        var timestring = r.getResponseHeader("DATE");

        // Set the time to the **slightly old** date sent from the 
        // server, then adjust it to a good estimate of what the
        // server time is **right now**.
        systemtime = new Date(timestring);
        systemtime.setMilliseconds(systemtime.getMilliseconds() + (latency / 2))
    };
    r.send(null);
}

Interesting aside: John Resig has a good article on the accuracy of Javascript timing.
It shouldn't cause a problem in this case, since you're only concerned about your time being off by ~1 second. A 15 ms difference shouldn't have much effect.

Hotze answered 14/5, 2012 at 15:17 Comment(4)
An amazing answer! Pretty sure this is the best solution available, but im going to add a bounty on to see if I can attract some other solutions.Tavia
This is how I would do it. The only difference being that I would do a sync call several times (i.e. 10 times) and then use the systemtime from the call with the lowest latency. Indeed, the higher the latency, the more impact you have from the fact that the distribution between from-client-to-server-time and from-server-to-client-time is not exactly 1:1.Fluky
Is there a better option than dividing response time by 2? In some of my tests the server blocks for awhile but then returns quickly. So maybe the trip time was 200ms, but the time returned is only 20ms off. Does that make sense?Montfort
Useful code. It's giving better results than "ServerDate" library. :)Blayne
I
21

How about a different approach: who cares about time? (You're not going to reliably sync the system clock with JavaScript.)

Instead, use a Node server with socket.io to synchronize when your clients advance the slideshow. Instead of the clients deciding when to advance, the server tells them to.

This approach comes with the added bonus of being able to manually fiddle with the slideshow while it's running. In the example that follows, I've added a Next button that causes all connected clients to immediately advance to the next slide.

app.js

var express = require('express')
    , app = express.createServer()
    , io = require('socket.io').listen(app)
    , doT = require('dot')
    , slide = 0
    , slides = [
        'http://placekitten.com/700/400?image=13',
        'http://placekitten.com/700/400?image=14',
        'http://placekitten.com/700/400?image=15',
        'http://placekitten.com/700/400?image=16',
        'http://placekitten.com/700/400?image=1',
        'http://placekitten.com/700/400?image=2',
        'http://placekitten.com/700/400?image=3',
        'http://placekitten.com/700/400?image=4',
        'http://placekitten.com/700/400?image=5',
        'http://placekitten.com/700/400?image=6',
        'http://placekitten.com/700/400?image=7',
        'http://placekitten.com/700/400?image=8',
        'http://placekitten.com/700/400?image=9',
        'http://placekitten.com/700/400?image=10',
        'http://placekitten.com/700/400?image=11',
        'http://placekitten.com/700/400?image=12',
    ];

app.listen(70); // listen on port 70

app.register('.html', doT); // use doT to render templates
app.set('view options', {layout:false}); // keep it simple
doT.templateSettings.strip=false; // don't strip line endings from template file

app.get('/', function(req, res) {
    res.render('index.html', { slide: slide, slides: slides });
});

app.post('/next', function(req, res) {
    next();
    res.send(204); // No Content
});

setInterval(next, 4000); // advance slides every 4 seconds

function next() {
    if (++slide >= slides.length) slide = 0;
    io.sockets.emit('slide', slide);
}

views/index.html

This file is processed as a doT template.

<!DOCTYPE html>
<html>
<head>
<title>Synchronized Slideshow</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
var curslide = {{=it.slide}}; // the slide the server is currently on.

$(function() {
    $('#slide' + curslide).css('left',0);

    $('#next').click(function() {
        $.post('/next');
    });
});

var socket = io.connect('http://localhost:70');
socket.on('slide', function(slide) {
    $('#slide' + curslide).animate({left:-700}, 400);
    $('#slide' + slide).css('left',700).show().animate({left:0}, 400);
    curslide = slide;
});
</script>
<style>
#slideshow, .slide { width:700px; height:400px; overflow:hidden; position:relative; }
.slide { position:absolute; top:0px; left:700px; }
</style>
</head>
<body>
    <div id="slideshow">
        {{~it.slides :url:i}}
            <div id="slide{{=i}}" class="slide"><img src="{{=url}}"></div>
        {{~}}
    </div>
    <button id="next">Next &gt;</button>
</body>
</html>

Copy these two files into a folder, then run

$ npm install express socket.io dot
$ node app.js

and navigate to http://localhost:70 in several different windows, then see the magic.

Itinerary answered 19/5, 2012 at 4:16 Comment(3)
Love this answer! Ignoring my question to get to the same objective. I'm going to take this approach and see what I can get out of it!Tavia
I got a couple of errors running this with the latest version of Express. Only when I pinned it to version 2.5.10 and reinstalled did it work. Also, I had to run sudo node app.jsWallaroo
Yeah, this is very old and express obviously has many breaking changes between v2 and v4. The need for sudo comes from listening on port 70, change that to something >1024 to fix.Itinerary
S
11

I'm glad you found a satisfactory answer to your question. I had a similar need to synchronize the browser with the server's clock and was determined to achieve it with better than 1 second accuracy like you were. I've written code to do this and am posting this answer here in case anyone else needs the solution too.

The code is called ServerDate and is freely available for download. Here's part of the README. Notice that I achieved 108 ms precision in my example:

You can use ServerDate as you would use the Date function or one of its instances, e.g.:

> ServerDate()
"Mon Aug 13 2012 20:26:34 GMT-0300 (ART)"

> ServerDate.now()
1344900478753

> ServerDate.getMilliseconds()
22

There is also a new method to get the precision of ServerDate's estimate of the server's clock (in milliseconds):

> ServerDate.toLocaleString() + " ± " + ServerDate.getPrecision() + " ms"
"Tue Aug 14 01:01:49 2012 ± 108 ms"

You can see the difference between the server's clock and the browsers clock, in milliseconds:

> ServerDate - new Date()
39
Stabilize answered 14/8, 2012 at 15:57 Comment(0)
F
0

You can't really sync up with the server. Measuring the time your server request needs (as MikeWyatt suggested) is not a good indicator on the latency.

Only your server knows when he responds a request. Therefore, it should send that information back with the answer. With Date.now() - new Date(timestringOfServerResponse) you can measure the latency exactly. Yet I'm not sure why you would need that value.

To sync an app between mulitiple devices, the server should send them which action to perform when. The "when" should not be "as soon as you get my response", but a exact timestamp. As far as the system clocks of your devices are accurate and synced (they usually are), the app will run its methods synchrounously, because it knows what to happen when (or at least: what should have happened then, and it can interpolate what to happen "now").

Frausto answered 14/5, 2012 at 23:14 Comment(3)
That's exactly how this is functioning. The page waits until the correct time before performing an action to ensure it is syncd across devices. The problem comes down to the fact that I have two iPads next to each other which are set to sync time with time servers, yet are 1 second apart.Tavia
So you want to implement a time server in javascript, to sync "custom system times"?Frausto
@Bergi, you're erroneously assuming that the client's clock matches the clock on the server. They are practically guaranteed to be different, probably by multiple seconds or minutes. Adjusting for latency isn't perfect, but it will get you pretty close. In the worst case, you'll be off by the full round-trip time, if one of the trips was instantaneous.Hotze
B
0

I'm extensively using the COMET pattern here for my real time web application.

To use that in your case you'd need the clients to open an AJAX request to the server and wait for an answer. As soon as it comes the client has to change slides.

On the server you have to hold back all answers till it's time to change slides. (You could be more advanced and delay afterwards on the client for the same time each, but that's most likely not necessary). I can't show you sample code for that here as I don't know what's available to you.

So you are effectively creating an orchestra where the server plays the conductor and all clients are listening to him.

Timing is then determined by the ability of the server to answer the requests at (nearly) the same time plus the network latency.
Usually the clients should be in the same part of the network so latency might be very similar - and the absolute value doesn't hurt here, only the variation.

And there might also be an additional trick helping: don't change the slides with a hard replacement, blend them. This will blur the change so that the eye can't catch the little timing differences that you'll always have.

(If you can't have the server playing conductor you're likely to have to use the solution by MikeWyatt - probably with a few requests and averaging the result, depending on the network setup. In a LAN one request might be enough, going over the full internet a bit over averaging won't hurt...)

Brotherson answered 19/5, 2012 at 12:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.