Sync JS time between multiple devices

2019-01-06 13:45发布

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?

5条回答
Ridiculous、
2楼-- · 2019-01-06 14:08

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.

查看更多
beautiful°
3楼-- · 2019-01-06 14:09

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...)

查看更多
劫难
4楼-- · 2019-01-06 14:13

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
查看更多
够拽才男人
5楼-- · 2019-01-06 14:16

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").

查看更多
相关推荐>>
6楼-- · 2019-01-06 14:17

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.

查看更多
登录 后发表回答