API Documentation for: 1.1.1
Show:

File:Captions.js

/**
*  @module cloudkid
*/
(function(){
	
	// Global classes to use, they will actually be imported in the constructor
	// so that we don't require a specific load order
	var Audio, OS;
	
	/**
	* A class that creates captioning for multimedia content. Captions are
	* created from a dictionary of captions and can be played by alias. Captions 
	* is a singleton class and depends on `cloudkid.Audio` for the progress update.
	*
	* @example
		var captionsDictionary = {
			"Alias1": [
				{"start":0, "end":2000, "content":"Ohh that looks awesome!"}
			],
			"Alias2": [
				{"start":0, "end":2000, "content":"Love it, absolutely love it!"}
			]
		};
	
		var captions = new cloudkid.Captions(captionsDictionary);
		captions.play("Alias1");
	*
	* @class Captions
	* @constructor
	* @param {Dictionary} [captionDictionary=null] The dictionary of captions
	* @param {createjs.Text} [field=null] An text field to use as the output for this captions object
	*/
	var Captions = function(captionDictionary, field)
	{
		// Import external classes
		Audio = cloudkid.Audio;
		OS = cloudkid.OS;

		this.initialize(captionDictionary, field);
	};
	
	/** 
	* Reference to the inherieted task 
	* 
	* @private
	* @property {Object} p
	*/
	var p = Captions.prototype;
	
	/** 
	* An object used as a dictionary with keys that should be the same as sound aliases
	* 
	* @private
	* @property {Dictionary} _captionDict
	*/
	p._captionDict = null;
	
	/** 
	* A reference to the CreateJS Text object that Captions should be controlling. 
	* Only one text field can be controlled at a time.
	* When using PIXI textfields, textIsProp should be false.
	*
	* @private
	* @property {createjs.Text|PIXI.Text|PIXI.BitmapText} _textField
	*/
	p._textField = null;
	
	/** 
	* The function to call when playback is complete. 
	*
	* @private
	* @property {Function} _completeCallback
	*/
	p._completeCallback = null;
	
	/** 
	* The collection of line objects {start:0, end:0, content:""} 
	* 
	* @private
	* @property {Array} _lines
	*/
	p._lines = null;
	
	/** 
	* The duration in milliseconds of the current sound. 
	*
	* @private
	* @property {int} _currentDuration
	*/
	p._currentDuration = 0;
	
	/** 
	* The current playback time 
	*
	* @private
	* @property {int} _currentTime
	*/
	p._currentTime = 0;
	
	/** 
	* Save the current line index 
	*
	* @private
	* @property {int} _currentLine 
	*/
	p._currentLine = -1;
	
	/** 
	* Cache the last active line
	*
	* @private
	* @property {int} _lastActiveLine
	*/
	p._lastActiveLine = -1;
	
	/** 
	* If we're playing 
	*
	* @private
	* @property {bool} _playing 
	*/
	p._playing = false;
	
	/** 
	* The singleton instance of Captions 
	*
	* @private
	* @property {Captions} _instance
	*/
	var _instance = null;
	
	/** 
	* If you want to mute the captions, doesn't remove the current caption 
	*
	* @private 
	* @property {bool} _muteAll
	*/
	var _muteAll = false;
	
	/**
	* If this Captions instance is a 'slave', that doesn't run cloudkid.Audio
	* and must have update() called manually (and passed milliseconds).
	* Default is false.
	*
	* @private
	* @property {bool} _isSlave
	*/
	p._isSlave = false;
	
	/**
	* If text should be set on the text field with '.text = ' instead of '.setText()'.
	* When using PIXI textfields, textIsProp should be false.
	* Default is true.
	*
	* @private
	* @property {bool} textIsProp
	*/
	p.textIsProp = true;

	/**
	* An animation timeline from Animator or PixiAnimator. This is used for syncing captions to audio that is synced
	* with with an animation.
	*
	* @private
	* @property {cloudkid.AnimatorTimeline|cloudkid.PixiAnimator.AnimTimeline} _animTimeline.
	*/
	p._animTimeline = null;
	
	/**
	* If this instance has been destroyed already 
	* 
	* @private
	* @property {bool} _isDestroyed
	*/
	p._isDestroyed = false;
	
	/** 
	* A bound update function to get the progress from Sound with 
	* 
	* @private
	* @property {Function} _boundUpdate
	*/
	p._boundUpdate = null;
	
	/** 
	* A bound completion callback for when Sound has finished playing. 
	* 
	* @private
	* @property {Function} _boundComplete
	*/
	p._boundComplete = null;
	
	/** 
	* The version number of this library 
	*
	* @public 
	* @property {String} VERSION
	* @static
	*/
	Captions.VERSION = "${version}";
	
	/** 
	* Creates the singleton instance of Captions, with an optional dictionary ready to go 
	*
	* @public
	* @method init
	* @param {object} [captionDictionary=null] An object set up in dictionary format of caption objects.
	* @param {createjs.Text} [field=null] An text field to use as the output for this captions object
	* @static
	*/
	Captions.init = function(captionDictionary, field)
	{
		_instance = new Captions(captionDictionary, field);
	};
	
	/**
	*  The singleton instance of Captions 
	*
	*  @static
	*  @readOnly
	*  @public
	*  @property {Captions} instance
	*/
	Object.defineProperty(
		Captions, "instance", 
		{
			get:function(){ return _instance; }
		}
	);
	
	/**
	* Constructor for caption.
	*
	* @private
	* @method initialize
	* @param [captionDictionary=null] An object set up in dictionary format of caption objects.
	* @param {createjs.Text|PIXI.Text|PIXI.BitmapText} [field=null] An text field to use as the output for this captions object. When using PIXI textfields, textIsProp should be false.
	*/
	p.initialize = function(captionDictionary, field)
	{
		this._lines = [];
		this.setDictionary(captionDictionary || null);
		this.setTextField(field);
		this._boundUpdate = this._updatePercent.bind(this);
		this._boundComplete = this._onSoundComplete.bind(this);
	};
	
	/**
	* Mute all of the captions.
	*
	* @public
	* @method setMuteAll
	* @param {bool} muteAll Whether to mute or unmute
	* @static
	*/
	Captions.setMuteAll = function(muteAll)
	{
		_muteAll = muteAll;
		
		if(_instance)
			_instance._updateCaptions();
	};
	
	/**
	* If the captions are all currently muted.
	*
	* @public
	* @method getMuteAll
	* @static
	* @return {bool} Whether the captions are all muted
	*/
	Captions.getMuteAll = function()
	{
		return _muteAll;
	};
	
	/**
	* Sets the dictionary object to use for captions. This overrides the current dictionary, if present.
	*
	* @public
	* @method setDictionary
	* @param {Dictionary} dict The dictionary object to use for captions.
	*/
	p.setDictionary = function(dict)
	{
		this._captionDict = dict;

		if(!dict) return;

		var timeFormat = /[0-9]+\:[0-9]{2}\:[0-9]{2}\.[0-9]{3}/;
		//Loop through each line and make sure the times are formatted correctly
		for(var alias in dict)
		{
			var lines = Array.isArray(dict[alias]) ? dict[alias] : dict[alias].lines;
			if(!lines)
			{
				Debug.log("alias '" + alias + "' has no lines!");
				continue;
			}
			for(var i = 0, len = lines.length; i < len; ++i)
			{
				var l = lines[i];
				if(typeof l.start == "string")
				{
					if(timeFormat.test(l.start))
						l.start = _timeCodeToMilliseconds(l.start);
					else
						l.start = parseInt(l.start, 10);
				}
				if(typeof l.end == "string")
				{
					if(timeFormat.test(l.end))
						l.end = _timeCodeToMilliseconds(l.end);
					else
						l.end = parseInt(l.end, 10);
				}
			}
		}
	};
	
	/** 
	* Sets the CreateJS Text or Pixi BitmapText/Text object that Captions should control the text of. 
	* Only one text field can be controlled at a time. When using PIXI textfields, textIsProp should be false.
	*
	* @public
	* @method setTextField
	* @param {createjs.Text|PIXI.Text|PIXI.BitmapText} field The CreateJS or PIXI Text object 
	*/
	p.setTextField = function(field)
	{
		if(this._textField)
		{
			if(this.textIsProp)
				this._textField.text = "";
			else
				this._textField.setText("");
		}
		this._textField = field || null;
	};
	
	/** 
	 * Returns if there is a caption under that alias or not. 
	 * 
	 * @method  hasCaption
	 * @param {String} alias The alias to check against
	 * @return {bool} Whether the caption was found or not
	 */
	p.hasCaption = function(alias)
	{
		return this._captionDict ? !!this._captionDict[alias] : false;
	};

	/** 
	 * A utility function for getting the full text of a caption by alias
	 * this can be useful for debugging purposes. 
	 * 
	 * @method  getFullCaption
	 * @param {String} alias The alias to get the text of
	 * @param {String} [separator=" "] The separation between each line
	 * @return {String} The entire captions concatinated by the separator
	 */
	p.getFullCaption = function(alias, separator)
	{
		if (!this._captionDict || !this._captionDict[alias]) return;

		separator = separator || " ";

		var result, 
			content, 
			lines = this._captionDict[alias].lines, 
			len = lines.length;

		for (var i = 0; i < len; i++)
		{
			content = lines[i].content;

			if (i === 0)
			{
				result = content;
			}
			else
			{
				result += separator + content;
			}
		}
		return result;
	};
	
	/**
	* Sets an array of line data as the current caption data to play.
	*
	* @private
	* @method _load
	* @param {String} data The string
	*/
	p._load = function(data)
	{
		if (this._isDestroyed) return;
		
		// Set the current playhead time
		this._reset();
		
		//make sure there is data to load, otherwise take it as an empty initialization
		if (!data)
		{
			this._lines = null;
			return;
		}
		this._lines = data.lines;
	};
	
	/**
	*  Reset the captions
	*  
	*  @private
	*  @method _reset
	*/
	p._reset = function()
	{
		this._currentLine = -1;
		this._lastActiveLine = -1;
	};
	
	/**
	*  Take the captions timecode and convert to milliseconds
	*  format is in HH:MM:ss:mmm
	*  
	*  @private
	*  @method _timeCodeToMilliseconds
	*  @param {String} input The input string of the format
	*  @return {int} Time in milliseconds
	*/
	function _timeCodeToMilliseconds(input)
	{
		var lastPeriodIndex = input.lastIndexOf(".");
		var ms = parseInt(input.substr(lastPeriodIndex + 1), 10);
		var parts = input.substr(0, lastPeriodIndex).split(":");
		var h = parseInt(parts[0], 10) * 3600000;//* 60 * 60 * 1000;
		var m = parseInt(parts[1], 10) * 6000;// * 60 * 1000;
		var s = parseInt(parts[2], 10) * 1000;
		
		return h + m + s + ms;
	}
	
	/**
	* The playing status.
	*
	* @public
	* @method isPlaying
	* @return {bool} If the caption is playing
	*/
	p.isPlaying = function()
	{ 
		return this._playing; 
	};
	
	/**
	*  Calculate the total duration of the current caption
	*  @private
	*  @method _getTotalDuration
	*/
	p._getTotalDuration = function()
	{
		var lines = this._lines;
		return lines ? lines[lines.length - 1].end : 0;
	};
	
	/**
	*  Get the current duration of the current caption
	*  @property {int} currentDuration
	*  @readOnly
	*/
	Object.defineProperty(p, "currentDuration",
	{
		get: function() { return this._currentDuration; }
	});

	/**
	*  If this Captions instance is a 'slave', that doesn't run cloudkid.Audio
	*  and must have update() called manually (and passed milliseconds).
	*  @property {bool} isSlave
	*  @default false
	*/
	Object.defineProperty(p, "isSlave",
	{
		get: function() { return this._isSlave; },
		set: function(isSlave) { this._isSlave = isSlave; }
	});
	
	/**
	*  Start the caption playback. Captions will tell cloudkid.Audio to play the proper sound.
	*  
	*  @public
	*  @method play
	*  @param {String} alias The desired caption's alias
	*  @param {function} callback The function to call when the caption is finished playing
	*  @return {function} The update function that should be called if captions isSlave is true
	*/
	p.play = function(alias, callback)
	{
		this.stop();
		this._completeCallback = callback;
		this._playing = true;
		this._load(this._captionDict[alias]);

		if (this._isSlave)
		{
			this._currentDuration = this._getTotalDuration();
		}
		else
		{
			this._currentDuration = Audio.instance.getLength(alias) * 1000;

			// Backward compatibility, but you should use the VOPlayer in the Sound or Audio libraries
			Audio.instance.play(alias, this._boundComplete, null, this._boundUpdate);
		}
		this.seek(0);

		if (this._isSlave)
		{
			return this._boundUpdate;
		}
	};
	
	/** 
	* Starts caption playback without controlling the sound or animation. Returns the update
	* function that should be called to control the Captions object.
	* @deprecated Use play(alias) instead, isSlave should be set to true
	* @public
	* @method run
	* @param {String} alias The caption/sound alias
	* @return {function} The update function that should be called
	*/
	p.run = function(alias)
	{
		if (!this._isSlave)
		{
			throw "Captions.isSlave needs to be set to tru to use run";
		}
		return this.play(alias);
	};

	/**
	* Runs a caption synced to the audio of an animation.
	* @deprecated Set Animator.captions or PixiAnimator.captions to set the captions object to use
	* @public
	* @method runWithAnimation
	* @param {cloudkid.AnimatorTimeline|cloudkid.PixiAnimator.AnimTimeline} animTimeline The animation to sync to.
	*/
	p.runWithAnimation = function(animTimeline)
	{
		if(!animTimeline.soundAlias) return;//make sure animation has audio to begin with.
		this.stop();
		this._animTimeline = animTimeline;
		this._load(this._captionDict[animTimeline.soundAlias]);
		OS.instance.addUpdateCallback("CK_Captions", this._updateToAnim.bind(this));
	};
	
	/** 
	* Is called when cloudkid.Audio finishes playing. Is not called if 
	* a cloudkid.AudioAnimation finishes playing, as then stop() is called.
	* 
	* @private
	* @method _onSoundComplete
	*/
	p._onSoundComplete = function()
	{
		var callback = this._completeCallback;
		
		this.stop();
		
		if(callback)
			callback();
	};
	
	/**
	* Convience function for stopping captions. Is also called by 
	* cloudkid.AudioAnimation when it is finished.
	*
	* @public
	* @method stop
	*/
	p.stop = function()
	{
		if(!this._isSlave && this._playing)
		{
			Audio.instance.stop();
			this._playing = false;
		}
		if(this._animTimeline)
		{
			this._animTimeline = null;
			OS.instance.removeUpdateCallback("CK_Captions");
		}
		this._lines = null;
		this._completeCallback = null;
		this._reset();
		this._updateCaptions();
	};
	
	/**
	* Goto a specific time.
	*
	* @public
	* @method seek
	* @param {int} time The time in milliseconds to seek to in the captions
	*/
	p.seek = function(time)
	{
		// Update the current time
		var currentTime = this._currentTime = time;
		
		var lines = this._lines;
		if(!lines)
		{
			this._updateCaptions();
			return;
		}
		
		if (currentTime < lines[0].start)
		{
			currentLine = this._lastActiveLine = -1;
			this._updateCaptions();
			return;
		}
		
		var len = lines.length;
		
		for(var i = 0; i < len; i++)
		{
			if (currentTime >= lines[i].start && currentTime <= lines[i].end)
			{
				this._currentLine = this._lastActiveLine = i;
				this._updateCaptions();
				break;
			}
			else if(currentTime > lines[i].end)
			{
				// this elseif helps us if there was no line at seek time, 
				// so we can still keep track of the last active line
				this._lastActiveLine = i;
				this._currentLine = -1;
			}
		}
	};

	/**
	* Callback for when a frame is entered, to sync to an animation's audio.
	*
	* @private
	* @method _updateToAnim
	*/
	p._updateToAnim = function()
	{
		//this should catch most interruptions to caption or animation playback, but if an animation is stopped before the sound plays, then this
		//might not catch it
		if(!this._animTimeline || //no longer have a timeline to use
			(!this._animTimeline.playSound && !this._animTimeline.soundInst) || //timeline has been cleaned up
			(this._animTimeline.soundInst && !this._animTimeline.soundInst.isValid))//audio on timeline is no longer valid
		{
			this.stop();
		}
		else if(this._animTimeline.soundInst)//make sure the audio instance exists - if it doesn't, it hasn't been played yet
		{
			this.seek(this._animTimeline.soundInst.position);
		}
	};
	
	/**
	* Callback for when a frame is entered.
	*
	* @private
	* @method _updatePercent
	* @param {number} progress The progress in the current sound as a percentage (0-1)
	*/
	p._updatePercent = function(progress)
	{
		if (this._isDestroyed) return;
		this._currentTime = progress * this._currentDuration;
		this._calcUpdate();
	};
	
	/**
	* Function to update the amount of time elapsed for the caption playback.
	* Call this to advance the caption by a given amount of time.
	*
	* @public
	* @method updateTime
	* @param {int} progress The time elapsed since the last frame in milliseconds
	*/
	p.updateTime = function(elapsed)
	{
		if (this._isDestroyed) return;
		this._currentTime += elapsed;
		this._calcUpdate();
	};
	
	/**
	* Calculates the captions after increasing the current time.
	*
	* @private
	* @method _calcUpdate
	*/
	p._calcUpdate = function()
	{
		var lines = this._lines;
		if(!lines)
			return;
		
		// Check for the end of the captions
		var len = lines.length;
		var nextLine = this._lastActiveLine + 1;
		var lastLine = len - 1;
		var currentTime = this._currentTime;
		
		// If we are outside of the bounds of captions, stop
		if (currentTime >= lines[lastLine].end)
		{
			this._currentLine = -1;
			this._updateCaptions();
		}
		else if (nextLine <= lastLine && currentTime >= lines[nextLine].start && currentTime <= lines[nextLine].end)
		{
			this._currentLine = this._lastActiveLine = nextLine;
			this._updateCaptions();
		}
		else if (this._currentLine != -1 && currentTime > lines[this._currentLine].end)
		{
			this._lastActiveLine = this._currentLine;
			this._currentLine = -1;
			this._updateCaptions();
		}
	};
	
	/**
	*  Updates the text in the managed text field.
	*  
	*  @private
	*  @method _updateCaptions
	*/
	p._updateCaptions = function()
	{
		if(this._textField)
		{
			var text = (this._currentLine == -1 || _muteAll) ? "" : this._lines[this._currentLine].content;
			if(this.textIsProp)
				this._textField.text = text;
			else
				this._textField.setText(text);
		}
	};
	
	/**
	*  Destroy this load task and don't use after this
	*  
	*  @method destroy
	*/
	p.destroy = function()
	{
		if (this._isDestroyed) return;
		
		this._isDestroyed = true;
		
		if(_instance === this)
			_instance = null;
		
		this._captionDict = null;
		this._lines = null;
	};
	
	// Assign to the namespacing
	namespace('cloudkid').Captions = Captions;
	
}());