// This should work both in node and in the browsers, so that's what this wrapper is about ;(function(root, undefined) { /** * Timecode object constructor * @param {number|String|Date|Object} timeCode Frame count as number, "HH:MM:SS(:|;|.)FF", Date(), or object. * @param {number} [frameRate=29.97] Frame rate * @param {boolean} [dropFrame=true] Whether the timecode is drop-frame or not * @constructor * @returns {Timecode} timecode */ var Timecode = function ( timeCode, frameRate, dropFrame ) { // Make this class safe for use without "new" if (!(this instanceof Timecode)) return new Timecode( timeCode, frameRate, dropFrame); // Get frame rate if (typeof frameRate === 'undefined') this.frameRate = 29.97; else if (typeof frameRate === 'number' && frameRate>0) this.frameRate = frameRate; else throw new Error('Number expected as framerate'); if (this.frameRate!==23.976 && this.frameRate!==24 && this.frameRate!==25 && this.frameRate!==29.97 && this.frameRate!==30 && this.frameRate!==50 && this.frameRate!==59.94 && this.frameRate!==60 ) throw new Error('Unsupported framerate'); // If we are passed dropFrame, we need to use it if (typeof dropFrame === 'boolean') this.dropFrame = dropFrame; else this.dropFrame = (this.frameRate===29.97 || this.frameRate===59.94); // by default, assume DF for 29.97 and 59.94, NDF otherwise // Now either get the frame count, string or datetime if (typeof timeCode === 'number') { this.frameCount = Math.round(timeCode); this._frameCountToTimeCode(); } else if (typeof timeCode === 'string') { // pick it apart var parts = timeCode.match('^([012]\\d):(\\d\\d):(\\d\\d)(:|;|\\.)(\\d\\d)$'); if (!parts) throw new Error("Timecode string expected as HH:MM:SS:FF or HH:MM:SS;FF"); this.hours = parseInt(parts[1]); this.minutes = parseInt(parts[2]); this.seconds = parseInt(parts[3]); // do not override input parameters if (typeof dropFrame !== 'boolean') { this.dropFrame = parts[4]!==':'; } this.frames = parseInt(parts[5]); this._timeCodeToFrameCount(); } else if (typeof timeCode === 'object' && timeCode instanceof Date) { var midnight = new Date(timeCode.getFullYear(), timeCode.getMonth(), timeCode.getDate(),0,0,0); var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000; var timecode_tz = timeCode.getTimezoneOffset() * 60 * 1000; this.frameCount = Math.round(((timeCode-midnight + (midnight_tz - timecode_tz))*this.frameRate)/1000); this._frameCountToTimeCode(); } else if (typeof timeCode === 'object' && timeCode.hours >= 0) { this.hours = timeCode.hours; this.minutes = timeCode.minutes; this.seconds = timeCode.seconds; this.frames = timeCode.frames; this._timeCodeToFrameCount(); } else if (typeof timeCode === 'undefined') { this.frameCount = 0; } else { throw new Error('Timecode() constructor expects a number, timecode string, or Date()'); } this._validate(timeCode); return this; }; /** * Validates timecode * @private * @param {number|String|Date|Object} timeCode for the reference */ Timecode.prototype._validate = function (timeCode) { // Make sure dropFrame is only for 29.97 & 59.94 if (this.dropFrame && this.frameRate!==29.97 && this.frameRate!==59.94) { throw new Error('Drop frame is only supported for 29.97 and 59.94 fps'); } // make sure the numbers make sense if (this.hours > 23 || this.minutes > 59 || this.seconds > 59 || this.frames >= this.frameRate || (this.dropFrame && this.seconds === 0 && this.minutes % 10 && this.frames < 2 * (this.frameRate / 29.97))) { throw new Error("Invalid timecode" + JSON.stringify(timeCode)); } }; /** * Calculate timecode based on frame count * @private */ Timecode.prototype._frameCountToTimeCode = function() { var fc = this.frameCount; // adjust for dropFrame if (this.dropFrame) { var df = this.frameRate===29.97 ? 2 : 4; // 59.94 skips 4 frames var d = Math.floor(this.frameCount / (17982*df/2)); var m = this.frameCount % (17982*df/2); if (m<df) m=m+df; fc += 9*df*d + df*Math.floor((m-df)/(1798*df/2)); } var fps = Math.round(this.frameRate); this.frames = fc % fps; this.seconds = Math.floor(fc/fps) % 60; this.minutes = Math.floor(fc/(fps*60)) % 60; this.hours = Math.floor(fc/(fps*3600)) % 24; }; /** * Calculate frame count based on time Timecode * @private */ Timecode.prototype._timeCodeToFrameCount = function() { this.frameCount = (this.hours*3600 + this.minutes*60 + this.seconds) * Math.round(this.frameRate) + this.frames; // adjust for dropFrame if (this.dropFrame) { var totalMinutes = this.hours*60 + this.minutes; var df = this.frameRate === 29.97 ? 2 : 4; this.frameCount -= df * (totalMinutes - Math.floor(totalMinutes/10)); } }; /** * Convert Timecode to String * @param {String} format output format * @returns {string} timecode */ Timecode.prototype.toString = function TimeCodeToString(format) { var frames = this.frames; var field = ''; if (typeof format === 'string') { if (format === 'field') { if (this.frameRate<=30) field = '.0'; else { frames = Math.floor(frames/2); field = '.'.concat((this.frameCount%2).toString()); }; } else throw new Error('Unsupported string format'); }; return "".concat( this.hours<10 ? '0' : '', this.hours.toString(), ':', this.minutes<10 ? '0' : '', this.minutes.toString(), ':', this.seconds<10 ? '0' : '', this.seconds.toString(), this.dropFrame ? ';' : ':', frames<10 ? '0' : '', frames.toString(), field ); }; /** * @returns {Number} the frame count when Timecode() object is used as a number */ Timecode.prototype.valueOf = function() { return this.frameCount; }; /** * Adds t to timecode, in-place (i.e. the object itself changes) * @param {number|string|Date|Timecode} t How much to add * @param {boolean} [negative=false] Whether we are adding or subtracting * @param {Number} [rollOverMaxHours] allow rollovers * @returns {Timecode} timecode */ Timecode.prototype.add = function (t, negative, rollOverMaxHours) { if (typeof t === 'number') { var newFrameCount = this.frameCount + Math.round(t) * (negative?-1:1); if (newFrameCount<0 && rollOverMaxHours > 0) { newFrameCount = (Math.round(this.frameRate*86400)) + newFrameCount; if (((newFrameCount / this.frameRate) / 3600) > rollOverMaxHours) { throw new Error('Rollover arithmetic exceeds max permitted'); } } if (newFrameCount<0) { throw new Error("Negative timecodes not supported"); } this.frameCount = newFrameCount; } else { if (!(t instanceof Timecode)) t = new Timecode(t, this.frameRate, this.dropFrame); return this.add(t.frameCount,negative,rollOverMaxHours); } this.frameCount = this.frameCount % (Math.round(this.frameRate*86400)); // wraparound 24h this._frameCountToTimeCode(); return this; }; Timecode.prototype.subtract = function(t, rollOverMaxHours) { return this.add(t,true,rollOverMaxHours); }; /** * Converts timecode to a Date() object * @returns {Date} date */ Timecode.prototype.toDate = function() { var ms = this.frameCount/this.frameRate*1000; var midnight = new Date(); midnight.setHours(0); midnight.setMinutes(0); midnight.setSeconds(0); midnight.setMilliseconds(0); var d = new Date( midnight.valueOf() + ms ); var midnight_tz = midnight.getTimezoneOffset() * 60 * 1000; var timecode_tz = d.getTimezoneOffset() * 60 * 1000; return new Date( midnight.valueOf() + ms + (timecode_tz-midnight_tz)); }; // Export it for Node or attach to root for in-browser /* istanbul ignore else */ if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { module.exports = Timecode; } else if (root) { root.Timecode = Timecode; } }(this));