// <reference path="../ScriptReference/jquery-1.7.1-vsdoc.js"/>
/// <reference path="Mediasite.Core.js"/>
/// <reference path="Mediasite.Player.Core.js"/>
/// <reference path="Mediasite.Player.Localization.js"/>
/// <reference path="../../Scripts/Mediasite.Player.UserActivation.js"/>
/*global Mediasite OnDSPlayStateChangeEvt : true, OnDSPositionChangeEvt : true, OnDSOpenStateChangeEvt : true, OnDSScriptCommandEvt : true, OnDSMediaErrorEvt : true */

Mediasite.namespace("MediaPlayer");
Mediasite.namespace("MediaPlayer.Views");

Mediasite.MediaPlayer.ModelFlash = function (viewOptions) {
    var $ = jQuery;
    var self = this;

    var element;
    var mediaElementObject;
    var allSources = [];
    var mediaSource, mediaServerType, mediaMimeType;

    var playbackRates = viewOptions.playbackRates;
    var skipBackAmount = 3000;
    var skipForwardAmount = 10000;
    var maxLiveSSReconnects = 15;
    var lastSeekTime = 0;
    var _manualSeekThreshold = -500;
    var ssSegmentLength = 2 * 1000;
    var volume = 100;
    var wasMutedBeforeLiveReset = false;
    var inactivityExceeded = false;
    var liveMediaWasReset = false;
    var playCount = 0;
    var playFrom = NaN;
    var wasMediaOpened = false;
    var isLively = false;
    var isStandAlone = viewOptions.isStandAlone;

    var liveStartTime;
    var liveStartOffset = 0;
    var liveStartOffsetPlayingOffset = 15000; // Approximate value, based on lab tests

    var positionUpdateTimer = null;
    var positionUpdateInterval = 200;

    var logger = Mediasite.Logging.LoggerFactory("Media XAP" + ":" + viewOptions.streamType, { separator: "\n" });

    var liveMediaOpenReconnector = new Mediasite.MediaPlayer.LiveMediaOpenReconnector();
    var mediaPlayerView;
    var mediaWrapper;
    var currentPlaybackRate = 1.0;

    var bufferIssueDetector;

    var unloadingPage = false;
    $(window).on('beforeunload', nowUnloading);
    function nowUnloading(e) {
        unloadingPage = true;
    }

    var clientBookmark = { position: 0, savedAt: 0 };
    var serverBookmark = { position: 0, savedAt: 0 };

    var _events = new Mediasite.Player.EventBundle();
    var _eventNames = Mediasite.MediaPlayer.ModelEvent;

    var disableVolume = viewOptions.disableVolume == undefined ? false : viewOptions.disableVolume;

    // OSMF uses these error codes.
    // It seems that the SmoothStreaming plugin uses others.
    var MediaErrorCodes = {
        ARGUMENT_ERROR: 4,
        ASYNC_ERROR: 3,
        DRM_SYSTEM_UPDATE_ERROR: 19,
        DVRCAST_CONTENT_OFFLINE: 21,
        DVRCAST_STREAM_INFO_RETRIEVAL_FAILED: 22,
        DVRCAST_SUBSCRIBE_FAILED: 20,
        F4M_FILE_INVALID: 23,
        HTTP_GET_FAILED: 6,
        IO_ERROR: 1,
        MEDIA_LOAD_FAILED: 7,
        NETCONNECTION_APPLICATION_INVALID: 12,
        NETCONNECTION_FAILED: 13,
        NETCONNECTION_REJECTED: 11,
        NETCONNECTION_TIMEOUT: 14,
        NETSTREAM_FILE_STRUCTURE_INVALID: 17,
        NETSTREAM_NO_SUPPORTED_TRACK_FOUND: 18,
        NETSTREAM_PLAY_FAILED: 15,
        NETSTREAM_STREAM_NOT_FOUND: 16,
        PLUGIN_IMPLEMENTATION_INVALID: 9,
        PLUGIN_VERSION_INVALID: 8,
        SECURITY_ERROR: 2,
        SOUND_PLAY_FAILED: 10,
        URL_SCHEME_INVALID: 5
    };


    function resetLiveMedia() {
        liveMediaWasReset = true;
        playCount = 0;

        var elementId = mediaElementObject.id;
        var jsBridgeFunctionGlobalName = makeJsBridgeFunctionGlobalName(elementId);

        volume = self.GetVolume();
        wasMutedBeforeLiveReset = self.IsMuted();

        element = mediaPlayerView.CreateMediaElement(elementId, jsBridgeFunctionGlobalName);
        mediaWrapper = mediaPlayerView.Wrapper;


        mediaElementObject = element.get(0);
    }

    function isLive() {
        return !!liveStartTime;
    }

    function reconnectOnLiveMediaOpenFailure() {
        resetLiveMedia();
    }

    function convertState(state) {
        switch (state.toLowerCase()) {
            case 'buffering':
                return Mediasite.Player.MediaState.Buffering;
            case 'closed':
                return Mediasite.Player.MediaState.Closed;
            case 'error':
                return Mediasite.Player.MediaState.Error;
            case 'opening':
                return Mediasite.Player.MediaState.Opening;
            case 'paused':
                return Mediasite.Player.MediaState.Paused;
            case 'playing':
                return Mediasite.Player.MediaState.Playing;
            case 'stopped':
                return Mediasite.Player.MediaState.Stopped;
            default:
                return Mediasite.Player.MediaState.Undefined;
        }
    }

    function notifyMediaLoaded() {
        var api = getStrobeApi();
        if (!api) { return; }

        var mediaInfo = makeMediaInfo(api);

        mediaPlayerView.UpdateMediaInfo(mediaInfo);
        _events.fire(_eventNames.MediaLoaded, mediaInfo);
    }

    function notifyMediaMetadataChanged() {
        var api = getStrobeApi();
        if (!api) { return; }

        var mediaInfo = makeMediaInfo(api);

        mediaPlayerView.UpdateMediaInfo(mediaInfo);
        _events.fire(_eventNames.MediaUpdated, mediaInfo);
    }

    function makeMediaInfo(strobeApi) {
        var aspectRatio = 0;
        if (strobeApi.getVideoHeight() > 0 && strobeApi.getVideoWidth() > 0) {
            aspectRatio = strobeApi.getVideoWidth() / strobeApi.getVideoHeight();
        }

        var mediaInfo = {
            AspectRatio: aspectRatio,
            Duration: self.GetDuration(),
            AudioOnly: strobeApi.getVideoHeight() === 0,
            CanChangeSpeed: false,
            CanFullScreen: false,
            CurrentLanguageIndex: self.GetCurrentAudioLanguageIndex(),
            Languages: self.GetAudioLanguages(),
            StreamType: viewOptions.streamType,
            IsMainStream: viewOptions.isMainStream
        };

        return mediaInfo;
    }

    function handleMediaEvent(event, param) {
        //var _scriptApi = getScriptApi();
        if (!_scriptApi) return;

        switch (event) {
            case "MediaOpened":
                if (liveMediaWasReset) {
                    // if media is being opened right after a reset on live, just play it right away
                    _scriptApi.Play();
                    liveMediaWasReset = false;
                    notifyMediaLoaded();
                } else {
                    wasMediaOpened = true;
                    mediaPlayerView.MediaLoadStart();
                    _events.fire(_eventNames.MediaLoadStart);
                    notifyMediaLoaded();
                }
                break;
            case "MediaEnded":
                _events.fire(_eventNames.PositionChanged, self.GetPosition());
                self.Stop();
                lastSeekTime = 0;
                _events.fire(_eventNames.PositionChanged, 0);
                _events.fire(_eventNames.StateChanged, Mediasite.Player.MediaState.MediaEnded);
                break;
            case "StateChanged":
                _events.fire(_eventNames.PositionChanged, self.GetPosition());
                var state = convertState(_scriptApi.CurrentState);
                switch (state) {
                    case Mediasite.Player.MediaState.Playing:
                        self.SetPlaybackRate(currentPlaybackRate);
                        if (liveStartTime && !_scriptApi.IsSmoothStreaming) {
                            if (_scriptApi.Position < 1000) {
                                // This operation expects that _scriptApi.Position == 0, which happens on _scriptApi.Stop()
                                // However, during buffering/playing hiccups, _scriptApi.Position does not reset
                                liveStartOffset = new Date() - liveStartTime - liveStartOffsetPlayingOffset;
                            }
                        }
                        //startPositionUpdateTimer();
                        break;
                    case Mediasite.Player.MediaState.Paused:
                    case Mediasite.Player.MediaState.Stopped:
                        //stopPositionUpdateTimer();
                        break;
                    default:
                        //stopPositionUpdateTimer();
                        break;
                }
                liveMediaOpenReconnector.setMediaState(state);
                _events.fire(_eventNames.StateChanged, state);
                break;
            case "MarkerReached":
                if (param.Type === "MS6") {
                    var actualMediaPosition = parseInt(param.Command, 10)
                    if (!isNaN(actualMediaPosition)) {
                        liveStartOffset = actualMediaPosition - _scriptApi.Position;
                    }
                }
                _events.fire(_eventNames.MediaEvent, param);
                break;
            case "RateChanged":
                var playbackRateInfo = {
                    Min: playbackRates[0],
                    Max: playbackRates[playbackRates.length - 1],
                    Current: Math.round(self.GetPlaybackRate() * 100) / 100
                };

                logger.Debug("Playback rate now " + playbackRateInfo.Current);
                _events.fire(_eventNames.PlaybackRateChanged, playbackRateInfo);
                break;
        }
    }

    function getStrobeApi() {
        if (mediaElementObject && mediaElementObject.jsBridgeInitialized) {
            return mediaElementObject;
        } else {
            return false;
        }
    }


    function notifyPlayerStateChange(state) {
        _events.fire(_eventNames.PositionChanged, self.GetPosition());
        liveMediaOpenReconnector.setMediaState(state);
        _events.fire(_eventNames.StateChanged, state);
    }

    // #region Strobe Handlers

    function jsBridge(id, eventName, data) {
        //toConsole('jsBridge --- id=' + id + '; eventName=' + eventName + '; data=' + data);

        var handlers = {
            onJavaScriptBridgeCreated: handle_onJavaScriptBridgeCreated,
            loadstart: handle_loadstart,
            emptied: handle_emptied,
            complete: handle_complete,
            durationchange: handle_durationchange,
            progress: handle_progress,
            timeupdate: handle_timeupdate,
            waiting: handle_waiting,
            play: handle_play,
            pause: handle_pause,
            seeking: handle_seeking,
            seeked: handle_seeked,
            loadedmetadata: handle_loadedmetadata
        };

        if (typeof handlers[eventName] === 'function') {
            handlers[eventName](data);
        }
    }

    function handle_onJavaScriptBridgeCreated(data) {

        toConsole('onJavaScriptBridgeCreated called');

        var swf = mediaElementObject;
        swf.jsBridgeInitialized = true;


        // # Set source URL.
        swf.setSrc(mediaSource);

        logger.Info("Media element source set", mediaSource, "of type", mediaMimeType, "on server type", mediaServerType);

        // # Set volume.
        self.SetVolume(volume);
        if (wasMutedBeforeLiveReset) {
            self.Mute();
        }
    }

    var hasLoadedMetadata = false;

    function handle_loadedmetadata(data) {
        // data = {
        //     videoHeight: 300, 
        //     readyState: 1, 
        //     videoWidth: 400
        // };

        toConsole('loadedmetadata ' + data.videoWidth + 'x' + data.videoHeight);

        var api = getStrobeApi();
        if (!api) { return; }

        hasLoadedMetadata = true;

        var aspect = data.videoWidth / data.videoHeight;

        if (liveMediaWasReset) {
            // if media is being opened right after a reset on live, just play it right away
            api.play2();
            liveMediaWasReset = false;
            notifyMediaLoaded();
        } else {
            wasMediaOpened = true;
            mediaPlayerView.MediaLoadStart();
            _events.fire(_eventNames.MediaLoadStart);
            notifyMediaLoaded();
        }
    }

    function handle_loadstart(data) {
        // data = {
        //     currentSrc: 'http://...',
        //     networkState: 2
        // };

        toConsole('loadstart ' + data.currentSrc);

        var api = getStrobeApi();
        if (!api) { return; }

        //api.load();
    }

    function handle_emptied(data) {
        // data = {
        //     duration: NaN,
        //     ended: false,
        //     error: null
        //     networkState: 0,
        //     paused: false,
        //     readyState: 0,
        //     seeking: false,
        //     src: 'http://...'
        // };

        toConsole('emptied ended: ' + data.ended);

        var isLive = !!liveStartTime;
        var isInEndedState = getEffectiveState() == Mediasite.Player.MediaState.MediaEnded;

        if (!hasLoadedMetadata) {
            // If we haven't even loadded the video, emptied doesn't mean much.
            return;
        }

        if (isInEndedState) {
            // We're already in MediaEnded state, don't mess with it.
            return;
        }

        if (isLive) {
            setEffectiveState(Mediasite.Player.MediaState.Stopped);
        } else {
            setEffectiveState(Mediasite.Player.MediaState.MediaEnded);
        }
    }

    function handle_complete(data) {
        // data = { ended: true };

        toConsole('complete');

        _events.fire(_eventNames.PositionChanged, self.GetPosition());
        //self.Stop();
        lastSeekTime = 0;
        _events.fire(_eventNames.PositionChanged, 0);

        setEffectiveState(Mediasite.Player.MediaState.MediaEnded);
    }

    function handle_durationchange(data) {
        // data = { duration: 360.0 };
        // NOTE: data.duration may be NaN.

        //toConsole('durationchange: ' + data.duration);

        // TODO: May need to notify with updated media info.    }
    }

    function handle_progress(data) {
        // data = {
        //     buffered: {
        //         length: 2,
        //         _start: [0, 90],
        //         _end:   [60, NaN],
        //     }
        // };

        //toConsole('progress');
        //toConsole('progress length: ' + data.buffered.length + ', start: ' + data.buffered._start[0] + ', end: ' + data.buffered._end[0]);
    }

    function handle_timeupdate(data) {

        // data = {
        //     duration: 0,
        //     currentTime: 0
        // };

        //toConsole('timeupdate');
        //toConsole('timeupdate: currentTime: ' + data.currentTime + '; duration: ' + data.duration);

        // bufferIssueDetector will not be instanciated if isLive() == false
        if (bufferIssueDetector && bufferIssueDetector.IsReady()) {
            bufferIssueDetector.TimeUpdateCalled(data.currentTime);
        }

        // PositionChanged event expects position values as prepared by self.GetPosition (as opposed to data.currentTime used above)
        _events.fire(_eventNames.PositionChanged, self.GetPosition(), true);
    }

    function handle_waiting(data) {
        // data = {};

        toConsole('waiting');

        if (getEffectiveState() == Mediasite.Player.MediaState.MediaEnded) {
            return;
        }

        setEffectiveState(Mediasite.Player.MediaState.Buffering);
    }

    function handle_play(data) {
        // data = {
        //     paused: false, 
        //     error: null, 
        //     ended: false
        // };

        toConsole('handle_play');

        var api = getStrobeApi();
        if (!api) { return; }

        //self.SetPlaybackRate(currentPlaybackRate);

        // TODO: If we ever need to play media that does not provide a 
        //       true timecode, we need to calculate liveStartOffset. 
        //       Smooth Streaming does not need this.
        //
        // liveStartOffset = new Date() - liveStartTime - liveStartOffsetPlayingOffset;

        setEffectiveState(Mediasite.Player.MediaState.Playing);

        playCount++;


        if (playCount == 1 && !isNaN(playFrom)) {
            // If we've remembered a playFrom position from an early seek 
            // and this is our first play, apply the seek now.
            self.SetPosition(playFrom);
        }
    }

    var effectiveState = Mediasite.Player.MediaState.Undefined;
    function getEffectiveState() {
        return effectiveState;
    };
    function setEffectiveState(state) {
        toConsole('SET EFFECTIVE STATE TO ' + state);

        effectiveState = state;
        notifyPlayerStateChange(state);
    };

    function handle_pause(data) {
        // data = { paused: true };

        toConsole('pause blah');

        var api = getStrobeApi();
        if (!api) { return; }

        if (getEffectiveState() == Mediasite.Player.MediaState.MediaEnded) {
            return;
        }

        if (liveStartTime) { // isLive
            //setEffectiveState(Mediasite.Player.MediaState.Stopped);
            return;
        }

        setEffectiveState(Mediasite.Player.MediaState.Paused);
    }

    function handle_seeking(data) {
        // data = { seeking: true };

        toConsole('seeking');
        _events.fire(_eventNames.SeekStarting);

        return false;
    }

    function handle_seeked(data) {
        // data = { seeking: false };

        toConsole('seeked');
        _events.fire(_eventNames.SeekEnding);
    }

    // #region Strobe playback error handling

    function onMediaPlaybackError(playerId, code, message, detail) {
        toConsole('%% onMediaPlaybackError %%  playerId: ' + playerId + ', code: ' + code + ', message: ' + message + ', detail: ' + detail);


        // Hide all Flash media elements.
        // Strobe insists on showing an error message in the media area, so we need to hide it.
        element.css('visibility', 'hidden');


        if (liveMediaOpenReconnector.HandleLiveMediaOpenFailure(reconnectOnLiveMediaOpenFailure)) {
            toConsole('ERROR will be handled by liveMediaOpenReconnector');
            return; // The error is being handled by the LiveMediaOpenReconnector.
        }


        var errorDescription;
        switch (code) {
            // TODO: Add cases for error codes that we want to handle specially here.                                                                              
            default:
                errorDescription = Mediasite.Player.Localization.MediaPlayer.GeneralError + " (" + code + " - " + message + ")";
                break;
        }
        _events.fire(_eventNames.MediaError, errorDescription);

    }


    function globalMediaPlaybackErrorFunction(playerId, code, message, detail) {
        var func = getMediaPlaybackErrorFunction(playerId);
        toConsole('onMediaPlaybackError function found');

        func(playerId, code, message, detail);
    }

    function setMediaPlaybackErrorFunction(playerId, func) {
        if (typeof playerId !== 'string') {
            throw { name: 'Invalid Argument', message: 'playerId must be a string.' };
        }

        if (typeof func !== 'function') {
            throw { name: 'Invalid Argument', message: 'func must be a function.' };
        }

        // Register our player specific error handler.
        window.strobeMediaErrorFunctions = window.strobeMediaErrorFunctions || {};
        window.strobeMediaErrorFunctions[playerId] = func;

        // Create the global error handler expected by Strobe.
        window.onMediaPlaybackError = window.onMediaPlaybackError || globalMediaPlaybackErrorFunction;
    }

    function getMediaPlaybackErrorFunction(playerId) {
        return window.strobeMediaErrorFunctions && window.strobeMediaErrorFunctions[playerId];
    }

    // #endregion

    // #endregion

    function makeJsBridgeFunctionGlobalName(elementId) {
        if (typeof elementId !== 'string') {
            throw { name: 'Invalid Argument', message: 'elementId must be a string.' };
        }

        window.strobeJsBridgeFunctions = window.strobeJsBridgeFunctions || {};
        window.strobeJsBridgeFunctions[elementId] = jsBridge;

        return 'window.strobeJsBridgeFunctions.' + elementId;
    }

    function makeUniqueSwfObjectId() {
        return 'swf_' + getUniqueString();
    }

    function getUniqueString() {
        // This is far from a perfect implementation.
        // It is not impossible for this to be called twice within on millisecond, 
        // returning the same string for both. In practice, I'm fairly confident 
        // this will not happen.
        return '' + new Date().getTime();
    }


    function getNextPlaybackRate(faster, loop) {
        /// <summary>Select the playback rate which is after (faster or slower) than the current playback rate</summary>
        /// <param name="faster" type="bool">True for faster playrate (default), false for a slower playrate</param>
        /// <param name="loop" type="bool">True for loop-around (speed > max ==> min and vice versa) (default), false for stopping at the min/max</param>

        // Sanitize parameters
        faster = faster !== false; // cast to boolean, undefined = true
        loop = loop !== false;  // cast to boolean, undefined = true

        // Get current playback rate
        // Use _scriptApi.PlaybackRate directly so that our fudging of 
        // 1.99 to 2.00 doesn't break the rate down keyboard shortcut.
        //var speed = Math.round(_scriptApi.PlaybackRate * 100) / 100; 
        var speed = Math.round(self.GetPlaybackRate() * 100) / 100;

        // Fetch and sanitize available playback rates (non-destructively)
        var toggleSteps = $.extend([], playbackRates);
        if (toggleSteps.length == 0) {
            toggleSteps = [1];
        }

        // If selecting a slower speed, scan the list in reverse.  (Mentally flip "min" and "max")
        var sortCoefficient = (faster ? 1 : -1);
        toggleSteps.sort(function (a, b) { return sortCoefficient * (a - b); });

        // Select the next playback rate
        if (loop && 0 >= sortCoefficient * (toggleSteps[toggleSteps.length - 1] - speed)) { // speed > max, accounting for faster/slower
            // Speed > max, so loop around to min 
            speed = toggleSteps[0];
        } else {
            // Select the speed faster/slower than the current speed
            $.each(toggleSteps, function (i, thisToggleStep) {
                if (0 >= sortCoefficient * (thisToggleStep - speed) // speed >= thisToggleSpeed, accounting for faster/slower
                    && 0 < sortCoefficient * (toggleSteps[i + 1] - speed)) { // speed < toggleSteps[i + 1], same
                    speed = toggleSteps[i + 1];
                    return false;
                }
            });
        }

        return speed;
    }

    function getPreviousSegmentBoundary(positionInMs) {
        return positionInMs - (positionInMs % ssSegmentLength);
    }


    // #region Public Methods

    this.SetupMediaElement = function (sources) {

        // The sources parameter is ignored, since we've already selected the 
        // diesired source earlier. This is part of the ongoing refactoring 
        // of the media player loading code.
        var source = viewOptions.selectedVideoSource;
        allSources = [source];

        mediaSource = source.Location;
        mediaServerType = source.ServerType;
        mediaMimeType = source.MimeType;

        if (typeof (mediaSource) === 'undefined') {
            _events.fire(_eventNames.MediaError, Mediasite.Player.Localization.MediaPlayer.NoCompatibleMediaError);
            logger.Error("Media - no compatible media source");
            return;
        }

        var elementId = makeUniqueSwfObjectId();
        var jsBridgeFunctionGlobalName = makeJsBridgeFunctionGlobalName(elementId);

        viewOptions.mediaSource = mediaSource;
        mediaPlayerView = $.extend({}, new Mediasite.MediaPlayer.BaseView(), new FlashView(viewOptions));
        element = mediaPlayerView.CreateMediaElement(elementId, jsBridgeFunctionGlobalName);
        mediaWrapper = mediaPlayerView.Wrapper;

        mediaElementObject = element.get(0);

        bufferIssueDetector = new BufferIssueDetector(getStrobeApi, logger);

        setMediaPlaybackErrorFunction(elementId, onMediaPlaybackError);
    };

    this.GetMediaWrapper = function () {
        return mediaWrapper;
    };

    this.SupportsPlayAsVideo = function () {
        return false;
    };

    this.AddEventHandler = function (eventName, handler) {
        _events.addHandler(eventName, handler);
    };

    this.UpdateOptions = function (options) {
        volume = options.Volume;
    };

    this.LiveStatusChange = function (status) {
        var ps = Mediasite.Player.PresentationStatus;
        isLively = (status == ps.Live || status == ps.LivePaused || status == ps.OpenForLive || status == ps.ScheduledForLive);

        liveMediaOpenReconnector.setLiveStatus(status);
        if (status === Mediasite.Player.PresentationStatus.LiveEnded) {
            self.Stop();
            setEffectiveState(Mediasite.Player.MediaState.MediaEnded);
            //_events.fire(_eventNames.StateChanged, Mediasite.Player.MediaState.Stopped);
        }
    };

    this.InactivityExceeded = function () {
        inactivityExceeded = true;
    };

    this.SetPosition = function (positionInMS) {
        if (inactivityExceeded === true) {
            window.location.reload();
            return;
        }

        var api = getStrobeApi();
        if (!api) { return; }

        if (playCount < 1) {
            // A seek before our first play is from applying the playFrom parameter. 
            // Strobe will not actually play from that position, so we need to remember 
            // this early seek value and apply it after first play.
            playFrom = Math.min(self.GetDuration(), Math.max(0, positionInMS));
            _events.fire(_eventNames.PositionChanged, playFrom);
            return;
        }

        if (positionInMS >= 0 && positionInMS <= self.GetDuration()) {
            lastSeekTime = positionInMS;

            // Smooth Streaming content (the only thing we now play in Flash) will seek to
            // a segment boundary. Unfortunately, if the seeked to time is closer to the 
            // next boundary than the previous boundary, the media element skips forward. 
            // For the sake of timed events, it's better to always play from the previous 
            // boundary than to skip a section of timeline. We will end up playing some 
            // extra content, but we can mask the position returned by GetPosition show 
            // the seeked to time until the media catches up.
            var previousSegmentBoundary = getPreviousSegmentBoundary(positionInMS);
            api.seek(previousSegmentBoundary / 1000); // Convert ms to seconds.

            _events.fire(_eventNames.PositionChanged, positionInMS);
        }
    };

    this.PlayFaster = function (doLoop) {
        doLoop = doLoop === true; // Default behavior: do not loop
        var playbackRate = getNextPlaybackRate(true, doLoop);
        self.SetPlaybackRate(playbackRate);
    };

    this.PlaySlower = function (doLoop) {
        doLoop = doLoop === true; // Default behavior: do not loop
        var playbackRate = getNextPlaybackRate(false, doLoop);
        self.SetPlaybackRate(playbackRate);
    };

    this.SkipBack = function () {
        var newPosition = self.GetPosition() - skipBackAmount;

        if (newPosition < 0) {
            newPosition = 0;
        }

        self.SetPosition(newPosition);
    };

    this.SkipForward = function () {
        var newPosition = self.GetPosition() + skipForwardAmount;

        if (newPosition < self.GetDuration()) {
            self.SetPosition(newPosition);
        }
    };

    this.FullScreen = function () {
        // Nuthin.
    };

    this.Pause = function () {
        toConsole('Pause called');
        var api = getStrobeApi();
        if (!api) { return; }

        if (self.GetDuration() > 0) {
            api.pause();
        } else {
            self.Stop();
        }

    };

    this.Play = function () {
        toConsole('Play called');
        var api = getStrobeApi();
        if (!api) { return; }

        // toConsole('(((((( getEffectiveState: ' + getEffectiveState() + ', getState: ' + api.getState() + ' ))))))');

        //        if (convertState(api.getState() == Mediasite.Player.MediaState.Paused) {
        //            toConsole('SKIP PLAY -- ALREADY BUFFERING');
        //            return;
        //        }

        if (inactivityExceeded === true) {
            window.location.reload();
        }
        else {
            toConsole('PLAYCOUNT = ' + playCount);
            if (isLive() && playCount > 0 && api.wasStopped) {
                resetLiveMedia();
            } else {
                try {
                    api.play2();
                }
                catch (e) { }
            }
            //playCount++;
        }

        if (bufferIssueDetector) {
            bufferIssueDetector.Reset();
        }
    };

    this.Stop = function () {
        toConsole('Stop called');
        var api = getStrobeApi();
        if (!api) { return; }

        try {
            api.wasStopped = true;
            api.stop2();
        } catch (e) { }

    };

    this.GetPosition = function () {
        var api = getStrobeApi();
        if (!api) { return; }

        if (playCount < 1 && !isNaN(playFrom)) {
            // If a playFrom position as set and we are still before the first play, 
            // we should return the playFrom position rather than the current position. 
            // This is because we are planning to seek to the playFrom position after 
            // the first play and we want the current position display to not be zero.
            return playFrom;
        }

        try {
            var position = (api.getCurrentTime() * 1000) + liveStartOffset; // Convert seconds to ms.

            if (lastSeekTime > 0) {
                var variance = position - lastSeekTime;
                if (_manualSeekThreshold < variance && variance < 0) {
                    // The media element was recently instructed to seek to a given position,
                    // but the media element is reporting a time slightly before that.
                    // However, for consistency, the model should report the requested position
                    // and assume the media will catch up (or seek elsewhere) soon.

                    return lastSeekTime;
                }
            }

            // Smooth Streaming content (the only thing we now play in Flash) will seek to
            // a segment boundary. We're going to mask that fact so that the time reported 
            // after a seek makes sense. This is important for timed events.
            if (position >= getPreviousSegmentBoundary(position) && position < lastSeekTime) {
                return lastSeekTime;
            }

            return position;

        } catch (e) {
            return 0;
        }
    };

    this.GetDuration = function () {
        // Non-Smooth Streaming live content results in a reported natural duration of 0.0, 
        // but Smooth Streaming live content results in a natural duration equal to the current position.
        // This throws off transport UI, so always report 0.0 for live content.
        var api = getStrobeApi();
        if (!api) { return; }

        try {
            return isLive() ? 0.0 : api.getDuration() * 1000; // Convert seconds to ms.
        } catch (e) {
            return 0.0;
        }
    };

    this.SetVolume = function (val) {
        var api = getStrobeApi();
        if (!api) { return; }

        if (disableVolume) {
            api.setVolume(0);
            api.setMuted(true);
        } else {
            api.setVolume(val / 100);
            api.setMuted(val == 0);
            _events.fire(_eventNames.VolumeChanged, Math.round(self.GetVolume()));
        }
    };

    this.GetVolume = function () {
        var api = getStrobeApi();
        if (!api) { return; }

        return api.getVolume() * 100;
    };

    this.StepVolumeUp = function () {
        var api = getStrobeApi();
        if (!api) { return; }

        if (!disableVolume) {
            if (api.getVolume() <= 0.8) {
                this.SetVolume((api.getVolume() + 0.2) * 100);
            } else {
                this.SetVolume(100);
            }
        }
    };

    this.StepVolumeDown = function () {
        var api = getStrobeApi();
        if (!api) { return; }

        if (!disableVolume) {
            if (api.getVolume() > 0.2) {
                this.SetVolume((api.getVolume() - 0.2) * 100);
            } else {
                this.SetVolume(0);
            }
        }
    };

    this.ToggleMute = function () {
        if (!disableVolume) {
            if (self.IsMuted()) {
                self.UnMute();
            } else {
                self.Mute();
            }
        }
    };

    this.Mute = function () {
        var api = getStrobeApi();
        if (!api) { return; }

        api.setMuted(true);

        if (self.IsMuted()) {
            _events.fire(_eventNames.VolumeChanged, 0);
        }
    };

    this.UnMute = function () {
        var api = getStrobeApi();
        if (!api) { return; }

        if (!disableVolume) {
            api.setMuted(false);

            if (self.GetVolume() === 0) {
                this.SetVolume(50);
            } else {
                _events.fire(_eventNames.VolumeChanged, Math.round(self.GetVolume()));
            }
        }
    };

    this.IsMuted = function () {
        var api = getStrobeApi();
        if (!api) { return; }

        return api.getMuted();
    };

    this.TogglePlayPause = function () {
        var strobeApi = getStrobeApi();
        if (!strobeApi) return;

        toConsole('TogglePlayPause ' + strobeApi.getState());

        // TODO: Get state from Strobe API.
        if (convertState(strobeApi.getState()) === Mediasite.Player.MediaState.Playing) {
            self.Pause();
        } else {
            self.Play();
        }
    };

    this.SetPlaybackRate = function (speed) {
        // TODO: Consider implementing if support for other media is added.
        return; // no-op, because Smooth Streaming is always 1.0x.
    };

    this.GetPlaybackRate = function () {
        // Smooth Streaming is always 1.0x.
        // TODO: Consider returning an actual value when 
        //       this media player supports other media.
        return 1.0;
    };

    this.GetCurrentAudioLanguageIndex = function () {
        // TODO: Return actual value.
        return 1; // Why is this hardcoded to one (not zero)?
    };

    this.GetAudioLanguages = function () {
        // TODO:Return actual value.
        return [];
    };

    this.SetAudioLanguage = function (language) {
        // no-op for now.
    };

    this.SetLiveStartTime = function (time) {
        liveStartTime = time;
        liveMediaOpenReconnector.setLiveStartTime(time);
    };

    this.RetrieveBookmark = function (bookmark) {
        clientBookmark = bookmark;
    };

    // #endregion


    function toConsole(message) {
        var prefix = '';
        var id;
        if (mediaElementObject && mediaElementObject.id) {
            id = mediaElementObject.id;
            prefix = id.substring(id.length - 2) + ' ';
        }

        //console.log(prefix + message);
    }



    //*****************************************************************
    //* FlashView
    //*
    //* options 
    //*  mediaContainer
    //*  backgroundElement
    //*  allowFullScreenVideo
    //*  playerLayoutOptions
    //* 
    //*****************************************************************
    var FlashView = function (options) {
        var element;
        var _scriptApi;
        var _isStandAlone = options && options.isStandAlone ? options.isStandAlone : false;
        var fakeVideoObject;
        this.Wrapper = undefined;
        var bigFakeElementId = 'big_shy_swf';
        var removeBigFakeAfter = 10 * 1000; // milliseconds

        var userActivation;

        this.CreateMediaElement = function (elementId, jsBridgeFunctionGlobalName) {

            options.mediaContainer.find('.MediaWrapper').remove();
            var $mediaWrapper = $('<div id="MediaWrapper" class="MediaWrapper"></div>').appendTo(options.mediaContainer);

            this.Wrapper = $mediaWrapper;
            var $backgroundElement = $(options.backgroundElement.selector);
            var backgroundColor = $backgroundElement.css('background-color', $backgroundElement.css('background-color')).css('background-color'); // leverage jQuery to get calculated color

            // Create video element.
            var element = makeVideoObject(elementId, jsBridgeFunctionGlobalName);

            if (options.height && options.width) {
                $(element).css({ height: options.height, width: options.width });
            }

            // Add element to DOM.
            $mediaWrapper.append(element);

            if (userActivation && userActivation.destroy) {
                userActivation.destroy();
            }
            userActivation = Mediasite.MediaPlayer.UserActivation.Create(this, options);

            if (Mediasite.BrowserDetect.Chrome()) {
                // Chrome pauses Flash content that it evaluates to be non-essential.
                // Typically, that means too small. To make auto-pausing of our video 
                // element less likely, we will add a full-window version of our 
                // Flash media player for ten seconds.
                // See BUG43627 and BUG44082.
                addUnpausingVideoObject();
            }

            return $(element);
        };

        this.UpdateMediaInfo = function (mediaInfo) {
            if (mediaInfo.AudioOnly) {
                element.css("height", "0px");
            }
        };

        function makeFlashVars(jsBridgeFunctionGlobalName) {

            function pushFlashParamsIntoVarList(flashParams) {
                var p;
                for (p in flashParams) {
                    if (flashParams.hasOwnProperty(p)) {
                        varList.push(p + '=' + flashParams[p]);
                    }
                }
            }

            var params = {
                autoPlay: true,
                plugin_AdaptiveStreamingPlugin: encodeURIComponent(getSSPluginPath()),
                AdaptiveStreamingPlugin_retryLive: true,
                AdaptiveStreamingPlugin_retryInterval: 45,
                controlBarMode: 'none',
                playButtonOverlay: false,
                bufferingOverlay: false,
                javascriptCallbackFunction: jsBridgeFunctionGlobalName
            };

            var varList = [];

            pushFlashParamsIntoVarList(params);

            return varList.join('&');
        }

        function addUnpausingVideoObject() {
            if (document.getElementById(bigFakeElementId)) {
                return;
            }

            fakeVideoObject = makeFakeVideoObject(bigFakeElementId);
            fakeVideoObject.setAttribute('style', 'position:absolute; top:0; left:0; z-index:-999; width:100%; height:100%; opacity:0.01');
            window.document.body.appendChild(fakeVideoObject);
            window.setTimeout(removeUnpausingVideoObject, removeBigFakeAfter);
        }

        function removeUnpausingVideoObject() {
            if (fakeVideoObject) {
                window.document.body.removeChild(fakeVideoObject);
            }
        }

        function makeGeneralVideoObject(elementId, flashVars, wmode) {
            var o;
            var swfPath = getSwfPath();

            if (isIELessThanNine()) {
                // IE8
                // 
                // IE 8 requires s movie param element to exist at the time 
                // the object element is created. Additionally, the swf URL 
                // must be unique (which affects multiple video).
                swfPath += '?ie8-uniquify=' + elementId;
                o = $('<object type="application/x-shockwave-flash" data="' + swfPath + '"><param name="movie" value="' + swfPath + '" /></object>').get(0);
            } else if (isIELessThanEleven()) {
                // IE 9-10
                // 
                // IE 10 and earlier won't load the swf if the data attribute
                // isn't populated when the object element is created. IE 11 
                // fails *unless* the data attribute is added afterward. -_-
                o = $('<object type="application/x-shockwave-flash" data="' + swfPath + '"></object>').get(0);
            } else {
                // IE11 and all non-IE browsers.
                o = document.createElement('object');
                o.setAttribute('type', 'application/x-shockwave-flash');
                o.setAttribute('data', swfPath);
            }

            o.setAttribute('id', elementId);
            o.setAttribute('name', elementId);
            o.setAttribute('width', '100%');
            o.setAttribute('height', '100%');


            addParam(o, 'flashvars', flashVars);
            addParam(o, 'allowFullScreen', 'false');
            addParam(o, 'wmode', wmode);
            addParam(o, 'SCALE', 'exactfit');

            return o;
        }

        function makeFakeVideoObject(elementId) {
            var o, flashVars;

            o = makeGeneralVideoObject(elementId, '', 'direct');
            o.setAttribute('style', 'position:absolute; top:0; left:0; z-index:-999; width:100%; height:100%; opacity:0.01');

            return o;
        }

        function makeVideoObject(elementId, jsBridgeFunctionGlobalName) {
            var o, movieVal, flashVars;

            o = makeGeneralVideoObject(elementId, makeFlashVars(jsBridgeFunctionGlobalName), getWmodeValue());
            o.setAttribute('class', 'MediaElement');

            return o;
        }

        function addParam(object, name, value) {
            var p = document.createElement('param');
            p.setAttribute('name', name);
            p.setAttribute('value', value);

            object.appendChild(p);
        }

        function isIELessThanEleven() {
            return parseInt(Mediasite.BrowserDetect.InternetExplorer()) < 11;
        }

        function isIELessThanNine() {
            return parseInt(Mediasite.BrowserDetect.InternetExplorer()) < 9;
        }

        function getWmodeValue() {
            // Too many problems with 'direct' wmode in too many browsers. We'll now use 'opaque' for everything.
            return 'opaque';
        }

        function getSwfPath() {
            // TODO: The differences between stand-alone and connected paths should be 
            //       handled by a strategy, not here.
            var relativeSwfPath = _isStandAlone ? 'StrobeMediaPlayback.2.0.swf' : 'Javascript/ClientBin/StrobeMediaPlayback.2.0.swf';

            return getClientBinPath() + relativeSwfPath;
        }

        function getSSPluginPath() {
            // TODO: The differences between stand-alone and connected paths should be 
            //       handled by a strategy, not here.
            var relativeSwfPath = _isStandAlone ? 'MSAdaptiveStreamingPlugin-v1.0.12-osmf2.0.swf' : 'Javascript/ClientBin/MSAdaptiveStreamingPlugin-v1.0.12-osmf2.0.swf';
            return getClientBinPath() + relativeSwfPath;
        }

        function getClientBinPath() {
            var pathEnd;
            var result = '';
            var sourcePath = $('script[src*="/Javascript"]').attr("src");

            if (sourcePath) {
                pathEnd = sourcePath.lastIndexOf('/Javascript');
                result = sourcePath.substring(0, pathEnd) + '/';
            }

            return result;
        }

        this.SetIsStandAlone = function (value) {
            _isStandAlone = value;
        };
    };

    // Occasionally the strobeApi Flash media element can get stuck in a 'buffering' state when live or a paused state when on-demand.  The end result is that handle_play is never 
    // called from the javascript bridge and the start/stop button no longer works.  This issue can occur when the browser is refreshed or when stop and then play are pressed during 
    // a live presentation.  The issue is sporadic, but happens enough to be an annoyance and make the player look bad.

    // The below component is called from handle_timeupdate.  It measures the amount of time between successive calls and polls the api for it's state once a small time
    // threshold is reached.  If the state is 'playing', then we are OK and the control is disbled until it is reset again.  If the state is 'buffering' or 'paused' 
    // (these are the two likely states the api finds itself in when the buffering issue happens), then we successively call pause() and play() on the api object.  
    // This alone is enough to nudge the media object and get it on its way.

    // BUG42357: 	Player: Flash player can get stuck in NotReady state
    // BUG43453:    On-demand Smooth Streaming Stuck On Pause But Still Plays

    var BufferIssueDetector = function (getStrobeApiCallback, logger) {

        // How much time should pass after the first timeValue reading before asking the api if it is playing 
        var bufferIssueTimeThreshold = 1;
        var firstTimeValue;
        var _bufferIssueMightExist = true;

        this.Reset = function () {
            _bufferIssueMightExist = true;
            firstTimeValue = null;
        };

        this.IsReady = function () {
            return _bufferIssueMightExist;
        };

        this.TimeUpdateCalled = function (timeValue) {

            if (!firstTimeValue) {
                firstTimeValue = timeValue;
            } else {
                if ((timeValue - firstTimeValue) > bufferIssueTimeThreshold) {
                    var strobeApi = getStrobeApiCallback();

                    if (strobeApi) {
                        var apiStatus = strobeApi.getState();

                        if (apiStatus == "playing") {
                            _bufferIssueMightExist = false;
                        } else if (apiStatus == "paused" || apiStatus == "buffering") {

                            strobeApi.pause();
                            strobeApi.play2();

                            _bufferIssueMightExist = false;
                        }
                    }
                }
            }
        };

    };

};

Mediasite.MediaPlayer.UserActivation.Register({
    mediaPlayerType: Mediasite.Player.MediaPlayerType.Flash,
    usable: function () {
        return Mediasite.BrowserDetect.Chrome();
    },
    constructor: function (mediaContainer, options) {
        var logger = Mediasite.Logging.LoggerFactory("MediaPlayer UserActivation", { separator: " - " });

        var loadFlashDelay = 2000;
        var pollDelay = 500;

        var zIndexData = 'CTA-z-index';

        var object = $(mediaContainer).find('object')[0];

        var checkIfPluginIsLoadedTimeout;
        var loadCheckInterval;

        var alreadyLoaded = 0;
        var userActivationAttempted = false;
        var userActivationEnabled = false;

        markUsable(false);

        checkIfPluginIsLoadedTimeout = setTimeout(function () {
            logger.Debug("Checking if media element is available (first attempt)");
            onLoadCheck();
        }, loadFlashDelay);


        function onLoadCheck() {
            if (isAvailable()) {
                logger.Debug("Media element appears loaded");
                markUsable(true);
            } else {
                enableUserActivation();
                clearInterval(loadCheckInterval);
                loadCheckInterval = setInterval(onLoadCheck, pollDelay);
            }
        }

        function enableUserActivation() {
            if (!userActivationAttempted) {
                userActivationAttempted = true;
                Mediasite.MediaPlayer.UserActivation.ShowPrompt({ top: '50px' }, onShowMessage);
                $(object).parent().on('click', stopPropagation);

                logger.Debug("Showing click-me prompt");
            }
        }

        var parent;
        function onShowMessage(messageContainer) {

            // Only one plugin <object> will have user activation enabled (ie. be made clickable).  
            // We need to know this fact and only call UserActivation.RemovePrompt() from reset() for the 
            // plugin where userActivationEnabled === true.

            userActivationEnabled = true;

            logger.Debug("Making media element clickable");
            css = $.extend({}, Mediasite.MediaPlayer.UserActivation.MediaElementCss, {
                position: 'fixed'
            });

            parent = object.parentNode;
            $(object)
                .appendTo(messageContainer)
                .css(css);
        }

        function reset() {
            clearTimeout(checkIfPluginIsLoadedTimeout);
            clearInterval(loadCheckInterval);

            // only re-parent and remove prompt (which deletes the container DIV) if this was the object 
            // that was made clickable.

            if (userActivationAttempted && userActivationEnabled) {
                userActivationAttempted = false;
                $(object).appendTo(parent);
                Mediasite.MediaPlayer.UserActivation.RemovePrompt();

                var resetCss = {};
                for (var prop in css) {
                    if (!css.hasOwnProperty(prop)) continue;
                    resetCss[prop] = '';
                }
                $(object).css(resetCss);

                logger.Debug("Removed 'click-me'");
            }
        }

        function markUsable(usable) {
            if (usable === void 0) usable = false;

            object.usable = usable;
            if (usable) {
                reset();
            }
        }

        function isAvailable() {
            var available = false;
            try {
                available = object && object.getState && (typeof object.getState() === 'string');
            } catch (err) {}
            return available;
        }

        function stopPropagation(e) {
            e.stopPropagation();
        }
    }
});
