@ircam/sync
Module that synchronises all clients to a server master clock.
Each client has access to a logical clock that synchronizes to the server
clock. The module also provides helper functions that allows to convert the
master clock, to and from, the local clock. Everybody can use the common master
clock to schedule synchronized events. A good practice is to convert to local
time at the last moment to trigger events, in order to minimize drift.
Table of Contents
Install
npm install [--save] @ircam/sync
Example use
This example show the usage of the library through a simple websocket transport with a naive ad-hoc ping / pong protocol.
Server-side
import { SyncServer } from '@ircam/sync';
const startTime = process.hrtime();
const getTimeFunction = () => {
const now = process.hrtime(startTime);
return now[0] + now[1] * 1e-9;
}
const syncServer = new SyncServer(getTimeFunction);
const wss = new ws.Server({ server: httpServer });
wss.on('connection', (socket) => {
const receiveFunction = callback => {
socket.on('message', request => {
request = JSON.parse(request);
if (request[0] === 0) {
const pingId = request[1];
const clientPingTime = request[2];
callback(pingId, clientPingTime);
}
});
};
const sendFunction = (pingId, clientPingTime, serverPingTime, serverPongTime) => {
const response = [
1,
pingId,
clientPingTime,
serverPingTime,
serverPongTime,
];
socket.send(JSON.stringify(response));
};
syncServer.start(sendFunction, receiveFunction);
});
Client-side
import { SyncClient } from '@ircam/sync';
const getTimeFunction = () => {
return performance.now() / 1000;
}
const syncClient = new SyncClient(getTimeFunction);
const socket = new WebSocket(url);
socket.addEventListener('open', () => {
const sendFunction = (pingId, clientPingTime) => {
const request = [
0,
pingId,
clientPingTime,
];
socket.send(JSON.stringify(request));
};
const receiveFunction = callback => {
socket.addEventListener('message', e => {
const response = JSON.parse(e.data);
if (response[0] === 1) {
const pingId = response[1];
const clientPingTime = response[2];
const serverPingTime = response[3];
const serverPongTime = response[4];
callback(pingId, clientPingTime, serverPingTime, serverPongTime);
}
});
}
const statusFunction = status => console.log(status);
syncClient.start(sendFunction, receiveFunction, statusFunction);
});
setInterval(() => {
const syncTime = syncClient.getSyncTime();
console.log(syncTime);
}, 100);
API
Classes
- SyncClient
SyncClient
instances synchronize to the clock provided
by the SyncServer instance. The default estimation behavior is
strictly monotonic and guarantee a unique convertion from server time
to local time.
- SyncServer
The SyncServer
instance provides a clock on which SyncClient
instances synchronize.
SyncClient
SyncClient
instances synchronize to the clock provided
by the SyncServer instance. The default estimation behavior is
strictly monotonic and guarantee a unique convertion from server time
to local time.
Kind: global class
See: SyncClient~start method to actually start a synchronisation
process.
new SyncClient(getTimeFunction, [options])
getTimeFunction | getTimeFunction | | |
[options] | Object | | |
[options.pingTimeOutDelay] | Object | | range of duration (in seconds) to consider a ping was not ponged back |
[options.pingTimeOutDelay.min] | Number | 1 | min and max must be set together |
[options.pingTimeOutDelay.max] | Number | 30 | min and max must be set together |
[options.pingSeriesIterations] | Number | 10 | number of ping-pongs in a series |
[options.pingSeriesPeriod] | Number | 0.250 | interval (in seconds) between pings in a series |
[options.pingSeriesDelay] | Number | | range of interval (in seconds) between ping-pong series |
[options.pingSeriesDelay.min] | Number | 10 | min and max must be set together |
[options.pingSeriesDelay.max] | Number | 20 | min and max must be set together |
[options.longTermDataTrainingDuration] | Number | 120 | duration of training, in seconds, approximately, before using the estimate of clock frequency |
[options.longTermDataDuration] | Number | 900 | estimate synchronisation over this duration, in seconds, approximately |
[options.estimationMonotonicity] | Boolean | true | When true , the estimation of the server time is strictly monotonic, and the maximum instability of the estimated server time is then limited to options.estimationStability . |
[options.estimationStability] | Number | 160e-6 | This option applies only when options.estimationMonotonicity is true. The adaptation to the estimated server time is then limited by this positive value. 80e-6 (80 parts per million, PPM) is quite stable, and corresponds to the stability of a conventional clock. 160e-6 is moderately adaptive, and corresponds to the relative stability of 2 clocks; 500e-6 is quite adaptive, it compensates 5 milliseconds in 1 second. It is the maximum value (estimationStability must be lower than 500e-6). |
syncClient.start(sendFunction, receiveFunction, reportFunction)
Start a synchronisation process by registering the receive
function passed as second parameter. Then, send regular messages
to the server, using the send function passed as first parameter.
Kind: instance method of SyncClient
sendFunction | sendFunction | |
receiveFunction | receiveFunction | to register |
reportFunction | reportFunction | if defined, is called to report the status, on each status change, and each time the estimation of the synchronised time updates. |
syncClient.stop()
Stop the synchronization process
Kind: instance method of SyncClient
syncClient.getLocalTime([syncTime]) ⇒ Number
Get local time, or convert a synchronised time to a local time.
Kind: instance method of SyncClient
Returns: Number
- local time, in seconds
[syncTime] | Number | Get local time according to given given syncTime , if syncTime is not defined returns current local time. |
syncClient.getSyncTime([localTime]) ⇒ Number
Get synchronised time, or convert a local time to a synchronised time.
Kind: instance method of SyncClient
Returns: Number
- synchronised time, in seconds.
[localTime] | Number | Get sync time according to given given localTime , if localTime is not defined returns current sync time. |
SyncClient~getTimeFunction ⇒ Number
Kind: inner typedef of SyncClient
Returns: Number
- strictly monotonic, ever increasing, time in second. When
possible the server code should define its own origin (i.e. time=0
) in
order to maximize the resolution of the clock for a long period of
time. When SyncServer~start
is called the clock should already be
running (cf. audioContext.currentTime
that needs user interaction to
start)
SyncClient~sendFunction : function
Kind: inner typedef of SyncClient
See: receiveFunction
pingId | Number | unique identifier |
clientPingTime | Number | time-stamp of ping emission |
SyncClient~receiveFunction : function
Kind: inner typedef of SyncClient
See: sendFunction
SyncClient~receiveCallback : function
Kind: inner typedef of SyncClient
pingId | Number | unique identifier |
clientPingTime | Number | time-stamp of ping emission |
serverPingTime | Number | time-stamp of ping reception |
serverPongTime | Number | time-stamp of pong emission |
SyncClient~reportFunction : function
Kind: inner typedef of SyncClient
report | Object | |
report.status | String | new , startup , training (offset adaptation), or sync (offset and speed adaptation). |
report.statusDuration | Number | duration since last status change. |
report.timeOffset | Number | time difference between local time and sync time, in seconds. |
report.frequencyRatio | Number | time ratio between local time and sync time. |
report.connection | String | offline or online |
report.connectionDuration | Number | duration since last connection change. |
report.connectionTimeOut | Number | duration, in seconds, before a time-out occurs. |
report.travelDuration | Number | duration of a ping-pong round-trip, in seconds, mean over the the last ping-pong series. |
report.travelDurationMin | Number | duration of a ping-pong round-trip, in seconds, minimum over the the last ping-pong series. |
report.travelDurationMax | Number | duration of a ping-pong round-trip, in seconds, maximum over the the last ping-pong series. |
SyncServer
The SyncServer
instance provides a clock on which SyncClient
instances synchronize.
Kind: global class
See: SyncServer~start method to
actually start a synchronisation process.
new SyncServer(function)
function | getTimeFunction | called to get the local time. It must return a time in seconds, monotonic, ever increasing. |
syncServer.start(sendFunction, receiveFunction)
Start a synchronisation process with a SyncClient
by registering the
receive function passed as second parameter. On each received message,
send a reply using the function passed as first parameter.
Kind: instance method of SyncServer
syncServer.getLocalTime([syncTime]) ⇒ Number
Get local time, or convert a synchronised time to a local time.
Kind: instance method of SyncServer
Returns: Number
- local time, in seconds
Note: getLocalTime
and getSyncTime
are basically aliases on the server.
[syncTime] | Number | Get local time according to given given syncTime , if syncTime is not defined returns current local time. |
syncServer.getSyncTime([localTime]) ⇒ Number
Get synchronised time, or convert a local time to a synchronised time.
Kind: instance method of SyncServer
Returns: Number
- synchronised time, in seconds.
Note: getLocalTime
and getSyncTime
are basically aliases on the server.
[localTime] | Number | Get sync time according to given given localTime , if localTime is not defined returns current sync time. |
SyncServer~getTimeFunction ⇒ Number
Kind: inner typedef of SyncServer
Returns: Number
- monotonic, ever increasing, time in second. When possible
the server code should define its own origin (i.e. time=0
) in order to
maximize the resolution of the clock for a long period of time. When
SyncServer~start
is called the clock should be running
(cf. audioContext.currentTime
that needs user interaction to start)
Example
const startTime = process.hrtime();
const getTimeFunction = () => {
const now = process.hrtime(startTime);
return now[0] + now[1] * 1e-9;
};
SyncServer~sendFunction : function
Kind: inner typedef of SyncServer
See: receiveFunction
pingId | Number | unique identifier |
clientPingTime | Number | time-stamp of ping emission |
serverPingTime | Number | time-stamp of ping reception |
serverPongTime | Number | time-stamp of pong emission |
SyncServer~receiveFunction : function
Kind: inner typedef of SyncServer
See: sendFunction
SyncServer~receiveCallback : function
Kind: inner typedef of SyncServer
pingId | Number | unique identifier |
clientPingTime | Number | time-stamp of ping emission |
Caveats
The synchronisation process is continuous: after a call to the start
method,
it runs in the background. It is important to avoid blocking it, on the client
side and on the server side.
In many cases, running the sync process in another thread is not an option as
the local clock will be different accross threads or processes.
Publication
For more information, you can read this article presented at the Web Audio Conference 2016:
Jean-Philippe Lambert, Sébastien Robaszkiewicz, Norbert Schnell. Synchronisation for Distributed Audio Rendering over Heterogeneous Devices, in HTML5. 2nd Web Audio Conference, Apr 2016, Atlanta, GA, United States. ⟨hal-01304889⟩ - https://hal.archives-ouvertes.fr/hal-01304889v1
Note: the stabilisation of the estimated synchronous time has been added after the publication of this article.
License
BSD-3-Clause. See the LICENSE file.