blob: 92456f259dae7eab75e8b99832489eb21765655a [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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.
//
////////////////////////////////////////////////////////////////////////////////
package mx.controls.videoClasses
{
import flash.events.Event;
import mx.controls.VideoDisplay;
import mx.core.mx_internal;
import mx.events.MetadataEvent;
import mx.managers.ISystemManager;
import mx.managers.SystemManager;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
use namespace mx_internal;
[ResourceBundle("controls")]
/**
* The CuePointManager class lets you use ActionScript code to
* manage the cue points associated with the VideoDisplay control.
*
* @see mx.controls.VideoDisplay
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class CuePointManager
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
private var _owner:VideoPlayer;
private var _metadataLoaded:Boolean;
private var _disabledCuePoints:Array;
private var _disabledCuePointsByNameOnly:Object;
private var _cuePointIndex:uint;
private var _cuePointTolerance:Number;
private var _linearSearchTolerance:Number;
private static var DEFAULT_LINEAR_SEARCH_TOLERANCE:Number = 50;
private var cuePoints:Array;
/**
* @private
* Reference to VideoDisplay object associated with this CuePointManager
* instance.
*/
mx_internal var videoDisplay:VideoDisplay;
/**
* @private
* Used for accessing localized Error messages.
*/
private var resourceManager:IResourceManager =
ResourceManager.getInstance();
//
// public APIs
//
/**
* Constructor.
*
* @param owner The VideoPlayer instance that is the parent of this CuePointManager.
* @param id This parameter is ignored; it is provided only for backwards compatibility.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function CuePointManager(owner:VideoPlayer, id:uint = 0)
{
// take in id just to be back-wards compatible
super();
_owner = owner;
reset();
_cuePointTolerance = _owner.playheadUpdateInterval / 2000;
_linearSearchTolerance = DEFAULT_LINEAR_SEARCH_TOLERANCE;
}
/**
* @private
* Reset cue point lists
*/
private function reset():void
{
_metadataLoaded = false;
cuePoints = null;
_disabledCuePoints = null;
_cuePointIndex = 0;
}
/**
* @private
* read only, has metadata been loaded
*/
private function get metadataLoaded():Boolean
{
return _metadataLoaded;
}
/**
* @private
* <p>Set by FLVPlayback to update _cuePointTolerance</p>
* Should be exposed in VideoDisplay/ here in Flex 2.0.
*/
private function set playheadUpdateInterval(aTime:Number):void
{
_cuePointTolerance = aTime / 2000;
}
/**
* Adds a cue point.
*
* <p>You can add multiple cue points with the same
* name and time. When you call the <code>removeCuePoint()</code> method
* with the name and time, it removes the first matching cue point.
* To remove all matching cue points, you have to make additional calls to
* the <code>removeCuePoint()</code> method.</p>
*
* @param cuePoint The Object describes the cue
* point. It must contain the properties <code>name:String</code>
* and <code>time:Number</code> (in seconds).
* If the Object does not conform to these
* conventions, it throws a <code>VideoError</code> error.
*
* @return A copy of the cue point Object added. The copy has the
* following additional properties:
*
* <ul>
* <li><code>array</code> - the Array of all cue points. Treat
* this Array as read only because adding, removing or editing objects
* within it can cause cue points to malfunction.</li>
*
* <li><code>index</code> - the index into the Array for the
* returned cue point.</li>
* </ul>
*
* @throws mx.controls.videoClasses.VideoError If the arguments are invalid.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addCuePoint(cuePoint:Object):Object
{
var message:String;
// make sense of param
var copy:Object = deepCopyObject(cuePoint);
// sanity check
var timeUndefined:Boolean = (isNaN(copy.time) || copy.time < 0);
if (timeUndefined)
{
message = resourceManager.getString(
"controls", "wrongTime");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
if (!copy.name)
{
message = resourceManager.getString(
"controls", "wrongName");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
// add cue point to cue point array
var index:Number;
copy.type = "actionscript";
if (cuePoints == null || cuePoints.length < 1)
{
index = 0;
cuePoints = [];
cuePoints.push(copy);
}
else
{
index = getCuePointIndex(cuePoints, true, copy.time, null, 0, 0);
index = (cuePoints[index].time > copy.time) ? 0 : index + 1;
cuePoints.splice(index, 0, copy);
}
// adjust _cuePointIndex
var now:Number = _owner.playheadTime;
if (now > 0)
{
if (_cuePointIndex == index)
{
if (now > cuePoints[index].time)
{
_cuePointIndex++;
}
}
else if (_cuePointIndex > index)
{
_cuePointIndex++;
}
}
else
{
_cuePointIndex = 0;
}
// return the cue point
var returnObject:Object = deepCopyObject(cuePoints[index]);
returnObject.array = cuePoints;
returnObject.index = index;
videoDisplay.dispatchEvent(new Event("cuePointsChanged"));
return returnObject;
}
/**
* Removes a cue point from the currently
* loaded FLV file. Only the <code>name</code> and <code>time</code>
* properties are used from the <code>cuePoint</code> argument to
* determine the cue point to be removed.
*
* <p>If multiple cue points match the search criteria, only
* one will be removed. To remove all cue points, call this function
* repeatedly in a loop with the same arguments until it returns
* <code>null</code>.</p>
*
* @param cuePoint The Object must contain at least one of
* <code>name:String</code> and <code>time:Number</code> properties, and
* removes the cue point that matches the specified properties.
*
* @return An object representing the cue point removed. If there was no
* matching cue point, then it returns <code>null</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeCuePoint(cuePoint:Object):Object
{
// bail if no cue points
if (cuePoints == null || cuePoints.length < 1)
return null;
// remove cue point from cue point array
var index:Number = getCuePointIndex(cuePoints, false, cuePoint.time, cuePoint.name, 0, 0);
if (index < 0)
return null;
cuePoint = cuePoints[index];
cuePoints.splice(index, 1);
// adjust _cuePointIndex
if (_owner.playheadTime > 0)
{
if (_cuePointIndex > index)
{
_cuePointIndex--;
}
}
else
{
_cuePointIndex = 0;
}
videoDisplay.dispatchEvent(new Event("cuePointsChanged"));
// return the cue point
return cuePoint;
}
/**
* @private
* removes enabled cue points from _disabledCuePoints
*/
private function removeCuePoints(cuePointArray:Array, cuePoint:Object):Number
{
var matchIndex:Number;
var matchCuePoint:Object;
var numChanged:Number = 0;
for (matchIndex = getCuePointIndex(cuePointArray, true, -1, cuePoint.name, 0, 0); matchIndex >= 0;
matchIndex = getNextCuePointIndexWithName(matchCuePoint.name, cuePointArray, matchIndex))
{
// remove match
matchCuePoint = cuePointArray[matchIndex];
cuePointArray.splice(matchIndex, 1);
matchIndex--;
numChanged++;
}
return numChanged;
}
/**
* @private
* inserts cue points into array
*/
private function insertCuePoint(insertIndex:Number, cuePointArray:Array, cuePoint:Object):Array
{
if (insertIndex < 0)
{
cuePointArray = [];
cuePointArray.push(cuePoint);
}
else
{
// find insertion point
if (cuePointArray[insertIndex].time > cuePoint.time)
{
insertIndex = 0;
}
else
{
insertIndex++;
}
// insert into sorted cuePointArray
cuePointArray.splice(insertIndex, 0, cuePoint);
}
return cuePointArray;
}
//
// package internal methods, called by FLVPlayback
//
/**
* @private
* <p>Called by FLVPlayback on "playheadUpdate" event
* to throw "cuePoint" events when appropriate.</p>
*/
mx_internal function dispatchCuePoints():void
{
var now:Number = _owner.playheadTime;
if (_owner.stateResponsive && cuePoints != null)
{
while (_cuePointIndex < cuePoints.length &&
cuePoints[_cuePointIndex].time <= now + _cuePointTolerance)
{
var metadataEvent:MetadataEvent =
new MetadataEvent(MetadataEvent.CUE_POINT);
metadataEvent.info = deepCopyObject(cuePoints[_cuePointIndex++]);
_owner.dispatchEvent(metadataEvent);
}
}
}
/**
* @private
* When our place in the stream is changed, this is called
* to reset our index into actionscript cue point array.
* Another method is used when cue points are added
* are removed.
*/
mx_internal function resetCuePointIndex(time:Number):void
{
if (time <= 0 || cuePoints == null)
{
_cuePointIndex = 0;
return;
}
var index:Number = getCuePointIndex(cuePoints, true, time, null, 0, 0);
_cuePointIndex = (cuePoints[index].time < time) ? index + 1 : index;
}
/**
* @private
* Search for a cue point in an array sorted by time. See
* closeIsOK parameter for search rules.
*
* @param cuePointArray array to search
* @param closeIsOK If true, the behavior differs depending on the
* parameters passed in:
*
* <ul>
*
* <li>If name is null or undefined, then if the specific time is
* not found then the closest time earlier than that is returned.
* If there is no cue point earlier than time, the first cue point
* is returned.</li>
*
* <li>If time is null, undefined or less than 0 then the first
* cue point with the given name is returned.</li>
*
* <li>If time and name are both defined then the closest cue
* point, then if the specific time and name is not found then the
* closest time earlier than that with that name is returned. If
* there is no cue point with that name and with an earlier time,
* then the first cue point with that name is returned. If there
* is no cue point with that name, null is returned.</li>
*
* <li>If time is null, undefined or less than 0 and name is null
* or undefined, a VideoError is thrown.</li>
*
* </ul>
*
* <p>If closeIsOK is false the behavior is:</p>
*
* <ul>
*
* <li>If name is null or undefined and there is a cue point with
* exactly that time, it is returned. Otherwise null is
* returned.</li>
*
* <li>If time is null, undefined or less than 0 then the first
* cue point with the given name is returned.</li>
*
* <li>If time and name are both defined and there is a cue point
* with exactly that time and name, it is returned. Otherwise null
* is returned.</li>
*
* <li>If time is null, undefined or less than 0 and name is null
* or undefined, a VideoError is thrown.</li>
*
* </ul>
* @param time search criteria
* @param name search criteria
* @param start index of first item to be searched, used for
* recursive implementation, defaults to 0 if undefined
* @param len length of array to search, used for recursive
* implementation, defaults to cuePointArray.length if undefined
* @returns index for cue point in given array or -1 if no match found
* @throws VideoError if time and/or name parameters are bad
* @see #cuePointCompare()
*/
private function getCuePointIndex(cuePointArray:Array, closeIsOK:Boolean,
time:Number, name:String,
start:Number, len:Number):Number
{
// sanity checks
if (cuePointArray == null || cuePointArray.length < 1)
return -1;
var timeUndefined:Boolean = (isNaN(time) || time < 0);
if (timeUndefined && !name)
{
var message:String = resourceManager.getString(
"controls", "wrongTimeName");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
if (len == 0) len = cuePointArray.length;
// name is passed in and time is undefined or closeIsOK is
// true, search for first name starting at either start
// parameter index or index at or after passed in time, respectively
if (name && (closeIsOK || timeUndefined))
{
var firstIndex:Number;
var index:Number;
if (timeUndefined)
firstIndex = start;
else
firstIndex = getCuePointIndex(cuePointArray, closeIsOK, time, null, 0, 0);
for (index = firstIndex; index >= start; index--)
if (cuePointArray[index].name == name)
break;
if (index >= start)
return index;
for (index = firstIndex + 1; index < len; index++)
if (cuePointArray[index].name == name)
break;
if (index < len)
return index;
return -1;
}
var result:Number;
// iteratively check if short length
if (len <= _linearSearchTolerance)
{
var max:Number = start + len;
for (var i:uint = start; i < max; i++)
{
result = cuePointCompare(time, name, cuePointArray[i]);
if (result == 0)
return i;
if (result < 0) break;
}
if (closeIsOK)
{
if (i > 0)
return i - 1;
return 0;
}
return -1;
}
// split list and recurse
var halfLen:Number = Math.floor(len / 2);
var checkIndex:Number = start + halfLen;
result = cuePointCompare(time, name, cuePointArray[checkIndex]);
if (result < 0)
return getCuePointIndex(cuePointArray, closeIsOK, time, name,
start, halfLen);
if (result > 0)
return getCuePointIndex(cuePointArray, closeIsOK, time, name,
checkIndex + 1, halfLen - 1 + (len % 2));
return checkIndex;
}
/**
* @private
* <p>Given a name, array and index, returns the next cue point in
* that array after given index with the same name. Returns null
* if no cue point after that one with that name. Throws
* VideoError if argument is invalid.</p>
*
* @returns index for cue point in given array or -1 if no match found
*/
private function getNextCuePointIndexWithName(name:String, array:Array, index:Number):Number
{
var message:String;
// sanity checks
if (!name)
{
message = resourceManager.getString(
"controls", "wrongName");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
if (!array)
{
message = resourceManager.getString(
"controls", "undefinedArray");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
if (isNaN(index) || index < -1 || index >= array.length)
{
message = resourceManager.getString(
"controls", "wrongIndex");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
// find it
var i:int;
for (i = index + 1; i < array.length; i++)
if (array[i].name == name)
break;
if (i < array.length)
return i;
return -1;
}
/**
* @private
* Takes two cue point Objects and returns -1 if first sorts
* before second, 1 if second sorts before first and 0 if they are
* equal. First compares times with millisecond precision. If
* they match, compares name if name parameter is not null or undefined.
*/
private static function cuePointCompare(time:Number, name:String, cuePoint:Object):Number
{
var compTime1:Number = Math.round(time * 1000);
var compTime2:Number = Math.round(cuePoint.time * 1000);
if (compTime1 < compTime2) return -1;
if (compTime1 > compTime2) return 1;
if (name != null)
{
if (name == cuePoint.name) return 0;
if (name < cuePoint.name) return -1;
return 1;
}
return 0;
}
/**
* @private
*
* <p>Search for a cue point in the given array at the given time
* and/or with given name.</p>
*
* @param closeIsOK If true, the behavior differs depending on the
* parameters passed in:
*
* <ul>
*
* <li>If name is null or undefined, then if the specific time is
* not found then the closest time earlier than that is returned.
* If there is no cue point earlier than time, the first cue point
* is returned.</li>
*
* <li>If time is null, undefined or less than 0 then the first
* cue point with the given name is returned.</li>
*
* <li>If time and name are both defined then the closest cue
* point, then if the specific time and name is not found then the
* closest time earlier than that with that name is returned. If
* there is no cue point with that name and with an earlier time,
* then the first cue point with that name is returned. If there
* is no cue point with that name, null is returned.</li>
*
* <li>If time is null, undefined or less than 0 and name is null
* or undefined, a VideoError is thrown.</li>
*
* </ul>
*
* <p>If closeIsOK is false the behavior is:</p>
*
* <ul>
*
* <li>If name is null or undefined and there is a cue point with
* exactly that time, it is returned. Otherwise null is
* returned.</li>
*
* <li>If time is null, undefined or less than 0 then the first
* cue point with the given name is returned.</li>
*
* <li>If time and name are both defined and there is a cue point
* with exactly that time and name, it is returned. Otherwise null
* is returned.</li>
*
* <li>If time is null, undefined or less than 0 and name is null
* or undefined, a VideoError is thrown.</li>
*
* </ul>
* @param timeOrCuePoint If String, then name for search. If
* Number, then time for search. If Object, then cuepoint object
* containing time and/or name parameters for search.
* @returns <code>null</code> if no match was found, otherwise
* copy of cuePoint object with additional properties:
*
* <ul>
*
* <li><code>array</code> - the array that was searched. Treat
* this array as read only as adding, removing or editing objects
* within it can cause cue points to malfunction.</li>
*
* <li><code>index</code> - the index into the array for the
* returned cuepoint.</li>
*
* </ul>
* @see #getCuePointIndex()
*/
private function getCuePoint(cuePointArray:Array, closeIsOK:Boolean,
timeNameOrCuePoint:Object = null):Object
{
var cuePoint:Object;
switch (typeof(timeNameOrCuePoint))
{
case "string":
cuePoint = {name:timeNameOrCuePoint};
break;
case "number":
cuePoint = {time:timeNameOrCuePoint};
break;
case "object":
cuePoint = timeNameOrCuePoint;
break;
} // switch
var index:Number = getCuePointIndex(cuePointArray, closeIsOK, cuePoint.time, cuePoint.name, 0, 0);
if (index < 0) return null;
cuePoint = deepCopyObject(cuePointArray[index]);
cuePoint.array = cuePointArray;
cuePoint.index = index;
return cuePoint;
}
/**
* @private
* <p>Given a cue point object returned from getCuePoint (needs
* the index and array properties added to those cue points),
* returns the next cue point in that array after that one with
* the same name. Returns null if no cue point after that one
* with that name. Throws VideoError if argument is invalid.</p>
*
* @returns <code>null</code> if no match was found, otherwise
* copy of cuePoint object with additional properties:
*
* <ul>
*
* <li><code>array</code> - the array that was searched. Treat
* this array as read only as adding, removing or editing objects
* within it can cause cue points to malfunction.</li>
*
* <li><code>index</code> - the index into the array for the
* returned cuepoint.</li>
*
* </ul>
*/
private function getNextCuePointWithName(cuePoint:Object):Object
{
var message:String;
// sanity checks
if (!cuePoint)
{
message = resourceManager.getString(
"controls", "undefinedParameter");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
if (isNaN(cuePoint.time) || cuePoint.time < 0)
{
message = resourceManager.getString(
"controls", "wrongTime");
throw new VideoError(VideoError.ILLEGAL_CUE_POINT, message);
}
// get index
var index:Number = getNextCuePointIndexWithName(cuePoint.name, cuePoint.array, cuePoint.index);
if (index < 0)
return null;
// return copy
var returnCuePoint:Object = deepCopyObject(cuePoint.array[index]);
returnCuePoint.array = cuePoint.array;
returnCuePoint.index = index;
return returnCuePoint;
}
/**
* Search for a cue point with specified name.
*
* @param name The name of the cue point.
*
* @return <code>null</code> if no match was found, or
* a copy of the matching cue point Object with additional properties:
*
* <ul>
* <li><code>array</code> - the Array of cue points searched. Treat
* this array as read only because adding, removing or editing objects
* within it can cause cue points to malfunction.</li>
*
* <li><code>index</code> - the index into the Array for the
* returned cue point.</li>
* </ul>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getCuePointByName(name:String):Object
{
return getCuePoint(cuePoints, false, name);
}
/**
* Returns an Array of all cue points.
*
* @return An Array of cue point objects.
* Each cue point object describes the cue
* point, and contains the properties <code>name:String</code>
* and <code>time:Number</code> (in seconds).
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getCuePoints():Array
{
return cuePoints;
}
/**
* Removes all cue points.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeAllCuePoints():void
{
cuePoints = null;
videoDisplay.dispatchEvent(new Event("cuePointsChanged"));
}
/**
* Set the array of cue points.
*
* <p>You can add multiple cue points with the same
* name and time. When you call the <code>removeCuePoint()</code> method
* with this name, only the first one is removed.</p>
*
* @param cuePointArray An Array of cue point objects.
* Each cue point object describes the cue
* point. It must contain the properties <code>name:String</code>
* and <code>time:Number</code> (in seconds).
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function setCuePoints(cuePointArray:Array):void
{
// sanity checks
if (cuePointArray == null)
return;
for (var index:uint = 0; index < cuePointArray.length; index++)
{
addCuePoint(cuePointArray[index]);
}
}
/**
* @private
* Used to make copies of cue point objects.
*/
private static function deepCopyObject(obj:Object, recurseLevel:Number = 0):Object
{
if (obj == null || typeof(obj) != "object") return obj;
if (isNaN(recurseLevel)) recurseLevel = 0;
var newObj:Object = {};
for (var i:Object in obj)
{
if (recurseLevel == 0 && (i == "array" || i == "index"))
{
// skip it
}
else if (typeof(obj[i]) == "object")
{
newObj[i] = deepCopyObject(obj[i], recurseLevel+1);
}
else
{
newObj[i] = obj[i];
}
}
return newObj;
}
} // class mx.controls.videoClasses.CuePointManager
}