Noncommital Premarital Mischief

    @ircam/sync

    2.1.0 • Public • Published

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

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

    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.

    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.

    Keywords

    none

    Install

    npm i @ircam/sync

    DownloadsWeekly Downloads

    4

    Version

    2.1.0

    License

    BSD-3-Clause

    Unpacked Size

    52.8 kB

    Total Files

    6

    Last publish

    Collaborators

    • jipodine
    • norbert.schnell
    • b-ma