@ircam/sync
v2.1.0
Published
Client / Server time synchronization component
Downloads
12
Readme
@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) => {
// the `receiveFunction` and `sendFunction` functions aim at abstracting
// the transport layer between the SyncServer and the SyncClient
const receiveFunction = callback => {
socket.on('message', request => {
request = JSON.parse(request);
if (request[0] === 0) { // this is a ping
// parse request
const pingId = request[1];
const clientPingTime = request[2];
// notify the SyncServer
callback(pingId, clientPingTime);
}
});
};
const sendFunction = (pingId, clientPingTime, serverPingTime, serverPongTime) => {
// create response object
const response = [
1, // this is a pong
pingId,
clientPingTime,
serverPingTime,
serverPongTime,
];
// send formatted response to the client
socket.send(JSON.stringify(response));
};
syncServer.start(sendFunction, receiveFunction);
});
Client-side
import { SyncClient } from '@ircam/sync';
// return the local time in second
const getTimeFunction = () => {
return performance.now() / 1000;
}
// init sync client
const syncClient = new SyncClient(getTimeFunction);
// init socket client
const socket = new WebSocket(url);
socket.addEventListener('open', () => {
const sendFunction = (pingId, clientPingTime) => {
const request = [
0, // this is a ping
pingId,
clientPingTime,
];
socket.send(JSON.stringify(request));
};
const receiveFunction = callback => {
socket.addEventListener('message', e => {
const response = JSON.parse(e.data);
if (response[0] === 1) { // this is a pong
const pingId = response[1];
const clientPingTime = response[2];
const serverPingTime = response[3];
const serverPongTime = response[4];
callback(pingId, clientPingTime, serverPingTime, serverPongTime);
}
});
}
// check the synchronization status, when this function is called for the
// first time, you can consider the synchronization process properly
// initiated.
const statusFunction = status => console.log(status);
// start synchronization process
syncClient.start(sendFunction, receiveFunction, statusFunction);
});
// monitor the synchronized clock
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.
Kind: global class
See: SyncClient~start method to actually start a synchronisation
process.
- SyncClient
- new SyncClient(getTimeFunction, [options])
- instance
- inner
- ~getTimeFunction ⇒ Number
- ~sendFunction : function
- ~receiveFunction : function
- ~receiveCallback : function
- ~reportFunction : function
new SyncClient(getTimeFunction, [options])
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| 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
| Param | Type | Description | | --- | --- | --- | | 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
| Param | Type | Description |
| --- | --- | --- |
| [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.
| Param | Type | Description |
| --- | --- | --- |
| [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
| Param | Type | Description | | --- | --- | --- | | pingId | Number | unique identifier | | clientPingTime | Number | time-stamp of ping emission |
SyncClient~receiveFunction : function
Kind: inner typedef of SyncClient
See: sendFunction
| Param | Type | Description | | --- | --- | --- | | receiveCallback | receiveCallback | called on each message matching messageType. |
SyncClient~receiveCallback : function
Kind: inner typedef of SyncClient
| Param | Type | Description | | --- | --- | --- | | 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
| Param | Type | Description |
| --- | --- | --- |
| 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.
- SyncServer
- new SyncServer(function)
- instance
- inner
- ~getTimeFunction ⇒ Number
- ~sendFunction : function
- ~receiveFunction : function
- ~receiveCallback : function
new SyncServer(function)
| Param | Type | Description | | --- | --- | --- | | 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
| Param | Type | | --- | --- | | sendFunction | sendFunction | | receiveFunction | receiveFunction |
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.
| Param | Type | Description |
| --- | --- | --- |
| [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.
| Param | Type | Description |
| --- | --- | --- |
| [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
| Param | Type | Description | | --- | --- | --- | | 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
| Param | Type | Description | | --- | --- | --- | | receiveCallback | receiveCallback | called on each message matching messageType. |
SyncServer~receiveCallback : function
Kind: inner typedef of SyncServer
| Param | Type | Description | | --- | --- | --- | | 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.