dvbcss-clocks
Version:
Javascript library implementing clock objects for modelling timelines. Useful in DVB CSS implementations.
722 lines (643 loc) • 23.5 kB
JavaScript
/****************************************************************************
* Copyright 2017 British Broadcasting Corporation
* and contributions Copyright 2017 British Telecommunications (BT) PLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* --------------------------------------------------------------------------
* Summary of parts containing contributions
* by British Telecommunications (BT) PLC:
* CorrelatedClock.prototype.setAtTime
* CorrelatedClock.prototype._rescheduleTimers
*****************************************************************************/
var EventEmitter = require("events");
var inherits = require('inherits');
var WeakMap = require('weak-map');
var PRIVATE = new WeakMap();
var nextIdNum = 0;
var nextTimeoutHandle = 0;
/**
* There has been a change in the timing of this clock.
* This might be due to a change made directly to this clock, or a change
* made to a parent in the hierarchy that affected this clock.
*
* <p>Causes of changes to clocks include: changes to
* [speed]{@link ClockBase#speed},
* [tick rate]{@link ClockBase#tickRate},
* [correlation]{@link CorrelatedClock#correlation}, or
* [parentage]{@link ClockBase#parent}.
* Changes to availability do not cause this event to fire.
*
* <p>The following parameters are passed as arguments to the event handler:
* @event change
* @param {ClockBase} source The clock that fired the event.
*/
/**
* This clock has become available.
*
* This might be because [availabilityFlag]{@link ClockBase#availabilityFlag}
* became true for this clock, or one of its parents in the hierarchy, causing this
* clock and all its parents to now be flagged as available.
*
* <p>The following parameters are passed as arguments to the event handler:
* @event available
* @param {ClockBase} source The clock that fired the event.
*/
/**
* This clock has become unavailable.
*
* This might be because [availabilityFlag]{@link ClockBase#availabilityFlag}
* became false for this clock, or one of its parents in the hierarchy.
*
* <p>The following parameters are passed as arguments to the event handler:
* @event unavailable
* @param {ClockBase} source The clock that fired the event.
*/
/**
* @module clocks
* @exports ClockBase
* @class ClockBase
*
* @classdesc
* Abstract Base class for clock implementations.
*
* <p>Implementations that can be used are:
* {@link DateNowClock} and
* {@link CorrelatedClock}.
*
* <p>This is the base class on which other clocks are implemented. It provides
* the basic framework of properties, getter and setters for common properties
* such as availability, speed, tick rate and parents, and provides the basic
* events framework, some standard helper methods for time conversion between clocks, comparisons
* between clocks and calculating disperison (error/uncertainty).
*
* <p>Clocks may fire the following events:
* <ul>
* <li> [change]{@link event:change}
* <li> [available]{@link event:available}
* <li> [unavailable]{@link event:unavailable}
* </ul>
*
* <p>Clock implementations should inherit from this class and implement some
* or all of the following method stubs:
* [now()]{@link ClockBase#now}
* [calcWhen()]{@link ClockBase#calcWhen}
* [getTickRate()]{@link ClockBase#getTickRate}
* [setTickRate()]{@link ClockBase#setTickRate}
* [getSpeed()]{@link ClockBase#getSpeed}
* [setSpeed()]{@link ClockBase#setSpeed}
* [getParent()]{@link ClockBase#getParent}
* [setParent()]{@link ClockBase#setParent}
* [toParentTime()]{@link ClockBase#toParentTime}
* [fromParentTime()]{@link ClockBase#fromParentTime}
* [_errorAtTime()]{@link ClockBase#_errorAtTime}
*
* @listens change
* @constructor
* @abstract
*/
var ClockBase = function() {
EventEmitter.call(this);
PRIVATE.set(this, {});
var priv = PRIVATE.get(this);
this._availability = true;
/**
* Every clock instance has a unique ID assigned to it for convenience. This is always of the form "clock_N" where N is a unique number.
* @var {String} id
* @memberof ClockBase
* @instance
*/
this.id = "clock_"+nextIdNum;
nextIdNum = nextIdNum+1;
priv.timerHandles = {};
this.on('change', this._rescheduleTimers.bind(this));
priv.availablePrev = this._availability;
};
inherits(ClockBase, EventEmitter);
/**
* @returns the current time value of this clock in units of ticks of the clock, or NaN if it cannot be determined (e.g. if the clock is missinga parent)
* @abstract
*/
ClockBase.prototype.now = function() {
throw "Unimplemented";
};
/**
* @var {Number} speed The speed at which this clock is running.
* 1.0 = normal. 0.0 = pause. negative values mean it ticks in reverse.
*
* For some implementations this can be changed, as well as read.
*
* <p>The underlying implementation of this property uses the
* [getSpeed]{@link ClockBase#getSpeed} and
* [setSpeed]{@link ClockBase#setSpeed} methods.
* @default 1.0
* @memberof ClockBase
* @instance
* @fires change
*/
Object.defineProperty(ClockBase.prototype, "speed", {
get: function() { return this.getSpeed(); },
set: function(v) { return this.setSpeed(v); },
});
/**
* @var {Number} tickRate The rate of this clock (in ticks per second).
*
* For some implementations this can be changed, as well as read.
*
* <p>The underlying implementation of this property uses the
* [getTickRate]{@link ClockBase#getTickRate} and
* [setTickRate]{@link ClockBase#setTickRate} methods.
*
* @memberof ClockBase
* @instance
* @fires change
*/
Object.defineProperty(ClockBase.prototype, "tickRate", {
get: function() { return this.getTickRate(); },
set: function(v) { return this.setTickRate(v); },
});
/**
* @var {ClockBase} parent The parent of this clock, or <tt>null</tt> if it has no parent.
*
* For some implementations this can be changed, as well as read.
*
* <p>The underlying implementation of this property uses the
* [getParent]{@link ClockBase#getParent} and
* [setParent]{@link ClockBase#setParent} methods.
*
* @memberof ClockBase
* @instance
* @fires change
*/
Object.defineProperty(ClockBase.prototype, "parent", {
get: function() { return this.getParent(); },
set: function(v) { return this.setParent(v); },
});
/**
* @var {Boolean} availabilityFlag The availability flag for this clock.
*
* For some implementations this can be changed, as well as read.
*
* <p>This is only the flag for this clock. Its availability may also be affected
* by the flags on its parents. To determine true availability, call the
* [isAvailable]{@link ClockBase#isAvailable} method.
*
* <p>The underlying implementation of this property uses the
* [getAvailabilityFlag]{@link ClockBase#getAvailabilityFlag} and
* [setAvailabilityFlag]{@link ClockBase#setAvailabilityFlag} methods.
*
* @default true
* @memberof ClockBase
* @instance
* @fires change
* @fires available
* @fires unavailable
*/
Object.defineProperty(ClockBase.prototype, "availabilityFlag", {
get: function() { return this.getAvailabilityFlag(); },
set: function(v) { return this.setAvailabilityFlag(v); },
});
/**
* Returns the current speed of this clock.
* @returns {Number} Speed of this clock.
* @abstract
*/
ClockBase.prototype.getSpeed = function() {
return 1.0;
};
/**
* Sets the current speed of this clock, or throws an exception if this is not possible
* @param {Number} newSpeed The new speed for this clock.
* @abstract
* @fires change
*/
ClockBase.prototype.setSpeed = function(newSpeed) {
throw "Unimplemented";
};
/**
* Calculates the effective speed of this clock, taking into account the effects
* of the speed settings for all of its parents.
* @returns {Number} the effective speed.
*/
ClockBase.prototype.getEffectiveSpeed = function() {
var s = 1.0;
var clock = this;
while (clock !== null) {
s = s * clock.getSpeed();
clock = clock.getParent();
}
return s;
};
/**
* Returns the current tick rate of this clock.
* @returns {Number} Tick rate in ticks/second.
* @abstract
*/
ClockBase.prototype.getTickRate = function() {
throw "Unimplemented";
};
/**
* Sets the current tick rate of this clock, or throws an exception if this is not possible.
* @param {Number} newRate New tick rate in ticks/second.
* @abstract
* @fires change
*/
ClockBase.prototype.setTickRate = function(newRate) {
throw "Unimplemented";
};
/**
* Return the current time of this clock but converted to units of nanoseconds, instead of the normal units of the tick rate.
* @returns {Number} current time of this clock in nanoseconds.
*/
ClockBase.prototype.getNanos = function() {
return this.now() * 1000000000 / this.getTickRate();
};
/**
* Convert a timevalue from nanoseconds to the units of this clock, given its current [tickRate]{@link ClockBase#tickRate}
* @param {Number} time in nanoseconds.
* @returns {Number} the supplied time converted to units of its tick rate.
*/
ClockBase.prototype.fromNanos = function(nanos) {
return nanos * this.getTickRate() / 1000000000;
};
/**
* Is this clock currently available? Given its availability flag and the availability of its parents.
* @returns {Boolean} True if this clock is available, and all its parents are available; otherwise false.
*/
ClockBase.prototype.isAvailable = function() {
var parent = this.getParent();
return this._availability && (!parent || parent.isAvailable());
};
/**
* Sets the availability flag for this clock.
*
* <p>This is only the flag for this clock. Its availability may also be affected
* by the flags on its parents. To determine true availability, call the
* [isAvailable]{@link ClockBase#isAvailable} method.
*
* @param {Boolean} availability The availability flag for this clock
* @fires unavailable
* @fires available
*/
ClockBase.prototype.setAvailabilityFlag = function(availability) {
this._availability = availability;
this.notifyAvailabilityChange();
};
/**
* Cause the "available" or "unavailable" events to fire if availability has
* changed since last time this method was called. Subclasses should call this
* to robustly generate "available" or "unavailable" events instead of trying
* to figure out if there has been a change for themselves.
* @fires unavailable
* @fires available
*/
ClockBase.prototype.notifyAvailabilityChange = function() {
var priv = PRIVATE.get(this);
var availableNow = this.isAvailable();
if (Boolean(availableNow) != Boolean(priv.availablePrev)) {
priv.availablePrev = availableNow;
this.emit(availableNow?"available":"unavailable", this);
}
};
/**
* Returns the availability flag for this clock (without taking into account whether its parents are available).
*
* <p>This is only the flag for this clock. Its availability may also be affected
* by the flags on its parents. To determine true availability, call the
* [isAvailable]{@link ClockBase#isAvailable} method.
*
* @returns {Boolean} The availability flag of this clock
*/
ClockBase.prototype.getAvailabilityFlag = function() {
return this._availability;
};
/**
* Convert a time value for this clock into a time value corresponding to teh underlying system time source being used by the root clock.
*
* <p>For example: if this clock is part of a hierarchy, where the root clock of the hierarchy is a [DateNowClock]{@link DateNowClock} then
* this method converts the supplied time to be in the same units as <tt>Date.now()</tt>.
*
* @param {Number} ticksWhen Time value of this clock.
* @return {Number} The corresponding time value in the units of the underlying system clock that is being used by the root clock, or <tt>NaN</tt> if this conversion is not possible.
* @abstract
*/
ClockBase.prototype.calcWhen = function(ticksWhen) {
throw "Unimplemented";
};
/**
* Return the root clock for the hierarchy that this clock is part of.
*
* <p>If this clock is the root clock (it has no parent), then it will return itself.
*
* @return {ClockBase} The root clock of the hierarchy
*/
ClockBase.prototype.getRoot = function() {
var p = this;
var p2 = p.getParent();
while (p2) {
p=p2;
p2=p.getParent();
}
return p;
};
/**
* Convert a time for the root clock to a time for this clock.
* @param {Number} t A time value of the root clock.
* @returns {Number} The corresponding time value for this clock.
*/
ClockBase.prototype.fromRootTime = function(t) {
var p = this.getParent();
if (!p) {
return t;
} else {
var x = p.fromRootTime(t);
return this.fromParentTime(x);
}
};
/**
* Convert a time for this clock to a time for the root clock.
* @param {Number} t A time value for this clock.
* @returns {Number} The corresponding time value of the root clock, or <tt>NaN</tt> if this is not possible.
*/
ClockBase.prototype.toRootTime = function(t) {
var p = this.getParent();
if (!p) {
return t;
} else {
var x = this.toParentTime(t);
return p.toRootTime(x);
}
};
/**
* Convert a time value for this clock to a time value for any other clock in the same hierarchy as this one.
* @param {ClockBase} otherClock The clock to convert the value value to.
* @param {Number} t Time value of this clock.
* @returns {Number} The corresponding time value for the specified <tt>otherClock</tt>, or <tt>NaN</tt> if this is not possible.
* @throws if this clock is not part of the same hierarchy as the other clock.
*/
ClockBase.prototype.toOtherClockTime = function(otherClock, t) {
var selfAncestry = this.getAncestry();
var otherAncestry = otherClock.getAncestry();
var clock;
var common = false;
while (selfAncestry.length && otherAncestry.length && selfAncestry[selfAncestry.length-1] === otherAncestry[otherAncestry.length-1]) {
selfAncestry.pop();
otherAncestry.pop();
common=true;
}
if (!common) {
throw "No common ancestor clock.";
}
selfAncestry.forEach(function(clock) {
t = clock.toParentTime(t);
});
otherAncestry.reverse();
otherAncestry.forEach(function(clock) {
t = clock.fromParentTime(t);
});
return t;
};
/**
* Get an array of the clocks that are the parents and ancestors of this clock.
* @returns {ClockBase[]} an array starting with this clock and ending with the root clock.
*/
ClockBase.prototype.getAncestry = function() {
var ancestry = [this];
var c = this;
while (c) {
var p = c.getParent();
if (p) {
ancestry.push(p);
}
c=p;
}
return ancestry;
};
/**
* Convert time value of this clock to the equivalent time of its parent.
*
* @param {Number} t Time value of this clock
* @returns {Number} corresponding time of the parent clock, or <tt>NaN</tt> if this is not possible.
* @abstract
*/
ClockBase.prototype.toParentTime = function(t) {
throw "Unimplemented";
};
/**
* Convert time value of this clock's parent to the equivalent time of this clock.
* @param {Number} t Time value of this clock's parent
* @returns {Number} corresponding time of this clock.
* @abstract
*/
ClockBase.prototype.fromParentTime = function(t) {
throw "Unimplemented";
};
/**
* Returns the parent of this clock, or <tt>null</tt> if it has no parent.
* @returns {ClockBase} parent clock, or <tt>null</tt>
* @abstract
*/
ClockBase.prototype.getParent = function() {
throw "Unimplemented";
};
/**
* Set/change the parent of this clock.
* @param {ClockBase} parent clock, or <tt>null</tt>
* @throws if it is not allowed to set this clock's parent.
* @abstract
* @fires change
*/
ClockBase.prototype.setParent = function(newParent) {
throw "Unimplemented";
};
/**
* Calculate the potential for difference between this clock and another clock.
* @param {ClockBase} otherClock The clock to compare with.
* @returns {Number} The potential difference in units of seconds. If effective speeds or tick rates differ, this will always be <tt>Number.POSITIVE_INFINITY</tt>
*
* If the clocks differ in effective speed or tick rate, even slightly, then
* this means that the clocks will eventually diverge to infinity, and so the
* returned difference will equal +infinity.
*
* If the clocks do not differ in effective speed or tick rate, then there will
* be a constant time difference between them. This is what is returned.
*/
ClockBase.prototype.clockDiff = function(otherClock) {
var thisSpeed = this.getEffectiveSpeed();
var otherSpeed = otherClock.getEffectiveSpeed();
if (thisSpeed !== otherSpeed) {
return Number.POSITIVE_INFINITY;
} else if (this.getTickRate() !== otherClock.getTickRate()) {
return Number.POSITIVE_INFINITY;
} else {
var root = this.getRoot();
var t = root.now();
var t1 = this.fromRootTime(t);
var t2 = otherClock.fromRootTime(t);
return Math.abs(t1-t2) / this.getTickRate();
}
};
/**
* Calculates the dispersion (maximum error bounds) at the specified clock time.
* This takes into account the contribution to error of this clock and its ancestors.
* @param {Number} t The time position of this clock for which the dispersion is to be calculated.
* @returns {Number} Dispersion (in seconds) at the specified clock time.
*/
ClockBase.prototype.dispersionAtTime = function(t) {
var disp = this._errorAtTime(t);
var p = this.getParent();
if (p) {
var pt = this.toParentTime(t);
disp += p.dispersionAtTime(pt);
}
return disp;
};
/**
* Calculates the error/uncertainty contribution of this clock at a given time position.
*
* <p>It is not intended that this function is called directly. Instead, call
* [dispersionAtTime()]{@link ClockBase.dispersionAtTime} which uses this function
* as part of calculating the total dispersion.
*
* @param {Number} t A time position of this clock
* @returns {Number} the potential for error (in seconds) arising from this clock
* at a given time of this clock. Does not include the contribution of
* any parent clocks.
*
* @abstract
*/
ClockBase.prototype._errorAtTime = function(t) {
throw "Unimplemented";
};
/**
* Retrieve the maximium frequency error (in ppm) of the root clock in the hierarchy.
*
* <p>This method contains an implementation for non-root clocks only. It must
* be overriden for root clock implementations.
*
* @returns {Number} The maximum frequency error of the root clock (in parts per million)
* @abstract
*/
ClockBase.prototype.getRootMaxFreqError = function() {
var root = this.getRoot();
if (root === this) {
throw "Unimplemented";
} else {
return root.getRootMaxFreqError();
}
};
/**
* A callback that is called when using [setTimeout]{@link ClockBase#setTimeout} or [setAtTime][@link ClockBase#setAtTime].
*
* @callback setTimeoutCallback
* @param {...*} args The parameters that were passed when the callback was scheduled.
* @this ClockBase
*/
/**
* Request a timeout callback when the time of this clock passes the current time plus
* the number of specified ticks.
*
* <p>If there are changes to timing caused by changes to this clock or its parents, then this timer will be automatically
* rescheduled to compensate.
*
* @param {setTimeoutCallback} func The function to callback
* @param {Number} ticks The callback is triggered when the clock passes (reaches or jumps past) this number of ticks beyond the current time.
* @param {...*} args Other arguments are passed to the callback
* @returns A handle for the timer. Pass this handle to [clearTimeout]{@link ClockBase#clearTimeout} to cancel this timer callback.
*/
ClockBase.prototype.setTimeout = function(func, ticks) {
arguments[1] = arguments[1] + this.now();
return this.setAtTime.apply(this, arguments);
};
/**
* Request a timeout callback when the time of this clock passes the specified time.
*
* <p>If there are changes to timing caused by changes to this clock or its parents, then this timer will be automatically
* rescheduled to compensate.
*
* @param {setTimeoutCallBack} func The function to callback
* @param {Number} when The callback is triggered when the clock passes (reaches or jumps past) this time.
* @param {...*} args Other arguments are passed to the callback
* @returns A handle for the timer. Pass this handle to [clearTimeout]{@link ClockBase#clearTimeout} to cancel this timer callback.
*/
ClockBase.prototype.setAtTime = function(func, when) {
var priv = PRIVATE.get(this);
var self = this;
var handle = self.id + ":timeout-" + nextTimeoutHandle++;
var root = self.getRoot();
if (root === null) {
root = self;
}
// remove first two args
var args = new Array(arguments.length-2);
for(var i=2; i<arguments.length; i++) {
args[i-2] = arguments[i];
}
var callback = function() {
delete priv.timerHandles[handle];
func.apply(self, args);
}
;
var numRootTicks = self.toRootTime(when) - root.now();
if (numRootTicks !== 0) {
numRootTicks = root.getSpeed() !== 0 ? numRootTicks / root.getSpeed() : NaN;
}
var millis = numRootTicks * (1000 / root.getTickRate());
var realHandle;
if (!isNaN(millis)) {
realHandle = setTimeout(callback, millis);
}
priv.timerHandles[handle] = { realHandle:realHandle, when:when, callback:callback };
return handle;
};
ClockBase.prototype._rescheduleTimers = function() {
// clock timing has changed, we need to re-schedule all timers
var priv = PRIVATE.get(this);
var root = this.getRoot();
for(var handle in priv.timerHandles) {
if (priv.timerHandles.hasOwnProperty(handle)) {
var d = priv.timerHandles[handle];
// clear existing timer
if (d.realHandle !== null && d.realHandle !== undefined) {
clearTimeout(d.realHandle);
}
// re-calculate when this timer is due and re-schedule
var numRootTicks = this.toRootTime(d.when) - root.now();
if (numRootTicks !== 0) {
numRootTicks = root.getSpeed() !== 0 ? numRootTicks / root.getSpeed() : NaN;
}
var millis = numRootTicks * (1000 / root.getTickRate());
if (!isNaN(millis)) {
d.realHandle = setTimeout(d.callback, Math.max(0,millis));
} else {
delete d.realHandle;
}
}
}
};
/**
* Clear (cancel) a timer that was scheduled using [setTimeout]{@link ClockBase#setTimeout} or [setAtTime][@link ClockBase#setAtTime].
*
* @param handle - The handle for the previously scheduled callback.
*
* If the handle does not represent a callback that was scheduled against this clock, then this method returns without doing anything.
*/
ClockBase.prototype.clearTimeout = function(handle) {
var priv = PRIVATE.get(this);
var d = priv.timerHandles[handle];
if (d !== undefined) {
clearTimeout(d.realHandle);
delete priv.timerHandles[handle];
}
};
module.exports = ClockBase;