Source: widgets/player/liveseeker.js

/**
 * The Seeker can be used to fast-forward or rewind a media-player.
 *
 * @example
 * var seeker = Seeker();
 *
 * seeker.attach(function onSpeedChanged (newSpeed) {
 *      // do something with the newSpeed, e.g. update the fast-forward or rewind player controls button
 * }, function onCurrentTimeUpdated (currentTime) {
 *      // do something with the currentTime, e.g. update the scrub bar
 * }, function onSeek (position) {
 *      // do something with the position, e.g. communicate it to the TimeLineManager
 * });
 *
 * // When the fast-forward player controls button is selected
 * seeker.fastForward();
 *
 * // When the rewind player controls button is selected
 * seeker.rewind():
 *
 * // When the play player controls button is selected (while seeking)
 * seeker.confirm();
 *
 * // When the seek action should be cancelled/reverted
 * seeker.cancel();
 *
 * // When the seeker is no longer used
 * seeker.detach();
 */
define('application/widgets/player/liveseeker', [
    'rofl/lib/utils',
    'antie/runtimecontext'
], function (
    Utils,
    RuntimeContext
) {
    'use strict';

    var application = RuntimeContext.getCurrentApplication(),
        config = application.getConfiguration().seeker || {},
        device = RuntimeContext.getDevice(),
        mediaPlayer = device.getMediaPlayer(),
        instance,

        /**
         * Interval value in milliseconds.
         *
         * @type {number} Milliseconds.
         */
        INTERVAL = config.interval || 500,

        /**
         * Speed divisor used for calculating the effective currentTime mutation.
         *
         * @type {number}
         */
        DIVISOR = INTERVAL / 1000,

        /**
         * Pre-defined speed steps.
         *
         * @type {string[]} Step.
         */
        STEPS = config.steps
            || ['2',
                '4',
                '8',
                '16',
                '32'],

        /**
         * The Seeker can be used to fast-forward or rewind a media-player.
         *
         * @name product-layer.player.widgets.Seeker
         * @class
         */
        Seeker = function () {

            this._playerStatus = null;
            this._onSpeedChanged = null;
            this._onCurrentTimeUpdated = null;

            this._steps = STEPS;
            this._interval = null;
            this._currentTime = null;
            this._speed = null;
            this._seekSteps = 0;

        };

    Seeker.prototype = {

        /**
         * Returns the value of a speed step that is to be added to the currentTime.
         *
         * @returns {number} Top be applied time mutation.
         * @private
         */
        _getSpeedStep: function _getSpeedStepFn () {
            var steps = this._steps,
                speed = steps[Math.abs(this._speed) - 1],
                multiplier = this._speed < 0 ? -1 : 1;

            return speed * multiplier;
        },

        /**
         * Handles speed changes.
         *
         * @private
         */
        _onSpeedChange: function _onSpeedChangeFn () {
            var onChangeVal = this._speed
                ? this._getSpeedStep()
                : null;

            if (this._onSpeedChanged) {

                this._onSpeedChanged(onChangeVal);
            } else {

                throw 'No _onSpeedChanged method defined.';
            }
        },

        /**
         * Handles current time changes.
         *
         * @private
         */
        _onCurrentTimeChange: function __onCurrentTimeChangeFn () {

            if (this._onCurrentTimeUpdated) {

                this._onCurrentTimeUpdated({
                    relativePlaybackTime: this._relativePlaybackTime,
                    currentTime: this._currentTime
                });
            } else {

                throw 'No _onCurrentTimeUpdated method defined.';
            }
        },

        /**
         * Stops the running interval.
         *
         * @private
         */
        _stopInterval: function _stopIntervalFn () {

            if (this._interval) {

                clearInterval(this._interval);
                this._interval = null;
            }
        },

        /**
         * Stops the current seeking action.
         *
         * @private
         */
        _onStopSeeking: function _onStopSeekingFn () {

            this._stopInterval();
            this._speed = null;
            this._onSpeedChange();
            this._onCurrentTimeChange();
        },

        /**
         * Handles interval callbacks.
         *
         * @private
         */
        _onInterval: function _onIntervalFn () {
            var step;

            if (this._speed) {

                step = Math.round(DIVISOR * this._getSpeedStep()) * 1000;

                this._relativePlaybackTime += step;
                this._currentTime += step;
                this._seekSteps += step;

                this._onCurrentTimeChange();
            }
        },

        /**
         * Starts the seeking interval.
         *
         * @private
         */
        _startInterval: function _startIntervalFn () {

            this._interval = setInterval(Utils.bind(this._onInterval, this)
                , INTERVAL);
        },

        /**
         * Gets triggered when the seeking starts.
         *
         * @private
         */
        _onStartSeeking: function _onStartSeekingFn () {

            this._oldPlaybackTime = this._relativePlaybackTime;
            this._oldCurrentTime = this._currentTime;

            mediaPlayer.pause();
            this._startInterval();
        },

        /**
         * Gets triggered right after seeking speed changes.
         *
         * @private
         */
        _onAfterSpeedMutation: function _onAfterSpeedMutationFn () {

            if (Math.abs(this._speed) <= this._steps.length) {

                this._onSpeedChange();
            } else { // Highest step passed, back to neutral state.

                this.confirm();
            }
        },

        /**
         * Gets triggered right before seeking speed changes.
         *
         * @param {int} direction - Seeking direction.
         * @private
         */
        _onBeforeSpeedMutation: function _onBeforeSpeedMutationFn (direction) {

            // Handle change of seeking direction
            if ((direction < 0 && this._speed > 0)
                || (direction > 0 && this._speed < 0)) {

                this._onStopSeeking();
            }

            // Set speed to neutral position
            if (this._speed === null) {

                this._speed = 0;
                this._onStartSeeking();
            }
        },

        /**
         * Sets the speed steps.
         *
         * @param {Array} steps - Speed steps.
         */
        setSpeedSteps: function setSpeedStepsFn (steps) {

            this._steps = steps || STEPS;
        },

        /**
         * Cancels the current seeking action.
         */
        cancel: function cancelFn () {

            this._relativePlaybackTime = this._oldPlaybackTime;
            this._currentTime = this._oldCurrentTime;
            this._onStopSeeking();
            mediaPlayer.resume();
        },

        /**
         * Confirms the current seeking position as resume point.
         *
         * @param {number} [time] - The time to seek to. Optional.
         */
        confirm: function confirmFn (time) {
            var difference,
                seekTo;

            if (time && time !== this._currentTime) {

                time = Math.round(time);

                difference = this._currentTime - time;
                this._seekSteps = -difference;
                this._relativePlaybackTime = time - this._startTime;
                this._currentTime = time;
            }

            seekTo = mediaPlayer.getCurrentTime() + (this._seekSteps / 1000);

            this._stopInterval();
            this._speed = null;

            this._onSeek({
                relativePlaybackTime: this._relativePlaybackTime,
                currentTime: this._currentTime,
                seekTo: seekTo
            });

            this._seekSteps = 0;
        },

        /**
         * Increase rewind seeking speed.
         */
        rewind: function rewindFn () {

            if (!this.canSeek()) {
                return;
            }

            this._onBeforeSpeedMutation(-1);
            this._speed--;
            this._onAfterSpeedMutation();
        },

        /**
         * Increase fast forward seeking speed.
         */
        fastForward: function fastForwardFn () {

            if (!this.canSeek()) {
                return;
            }

            this._onBeforeSpeedMutation(1);
            this._speed++;
            this._onAfterSpeedMutation();
        },

        /**
         * Whether seeking is active or not.
         *
         * @returns {boolean} Active.
         */
        isActive: function isActiveFn () {

            return !!this._interval;
        },

        /**
         * Returns whether seeking is allowed or not.
         *
         * @returns {boolean} Can seek.
         */
        canSeek: function canSeekFn () {

            return !!mediaPlayer.getSeekableRange();
        },

        /**
         * Detaches the seeker from the media player.
         */
        detach: function detachFn () {

            this._stopInterval();
            this._onSpeedChanged = null;
            this._onCurrentTimeUpdated = null;

            this._steps = null;
            this._interval = null;
            this._currentTime = null;
            this._duration = null;
            this._speed = null;
        },

        /**
         * Sets the current program time.
         *
         * @param {number} startTime - The start time of the program.
         * @param {number} endTime - The end time of the program.
         * @param {number} currentPlaybackTime - The current playback time.
         */
        setProgramTime: function (startTime, endTime, currentPlaybackTime) {
            this._startTime = startTime;
            this._endTime = endTime;
            this._currentTime = currentPlaybackTime;

            this._relativePlaybackTime = currentPlaybackTime - startTime;
        },

        /**
         * Returns the seek direction.
         *
         * @returns {number} - Returns -1|1. 1 if the direction is forward, -1 if the direction is backwards.
         */
        getDirection: function () {
            return this._speed > 0 ? 1 : -1;
        },

        /**
         * Sets the current time.
         *
         * @param {number} currentTime - The current time.
         */
        setCurrentTime: function (currentTime) {
            this._currentTime = currentTime;

            this._relativePlaybackTime = currentTime - this._startTime;
        },

        /**
         * Attaches the seeker on the media player.
         *
         * @param {Object} opts - The attach options.
         * @param {Function} opts.onSpeedChanged - Handles on speed changed events.
         * @param {Function} opts.onCurrentTimeUpdated - Handles on current time updated events.
         * @param {Function} opts.onSeek - Gets triggered on confirming the seek action.
         * @param {Array} [opts.steps] - Steps used for the different speeds.
         */
        attach: function attachFn (opts) {
            opts = opts || {};

            this._onSpeedChanged = opts.onSpeedChanged;
            this._onCurrentTimeUpdated = opts.onCurrentTimeUpdated;
            this._onSeek = opts.onSeek;
            this.setSpeedSteps(opts.steps);
        }
    };

    return function () {

        if (!instance) {

            instance = new Seeker();
        }

        return instance;
    };

});