// <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"/>
/*global Mediasite OnDSPlayStateChangeEvt : true, OnDSPositionChangeEvt : true, OnDSOpenStateChangeEvt : true, OnDSScriptCommandEvt : true, OnDSMediaErrorEvt : true */

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

Mediasite.MediaPlayer.ModelHtml5Dash = function (viewOptions) {
    /// <summary>Media player model for HTML5 MPEG-DASH media element</summary>
    /// <param name="options" type="object">{ playbackRates: (array) }</param>
    var $ = jQuery;
    var self = this;

    var SEGMENT_LENGTH_TO_UNSAFE_SECONDS = {
        2: 18,
        4: 28,
        6: 38,
        8: 48,
        10: 58
    };
    var segmentLengthSeconds = (viewOptions.selectedVideoSource.SegmentLength || 0) / 1000;
    var liveStartUnsafeSeconds = Mediasite.MediaPlayer.calculateLiveStartUnsafeWindow(SEGMENT_LENGTH_TO_UNSAFE_SECONDS, segmentLengthSeconds);

    var element;
    var mediaElement;
    var mediaSources;
    var lastLoadedMediaSource;
    var isSetUp = false;
    var defaultPlaybackRate = 1;
    var playbackRates = viewOptions.playbackRates;
    var skipBackAmount = 3000;
    var skipForwardAmount = 10000;
    var lastSeekTime = 0;
    var lastSeekTimeDate = new Date();
    var manualSeekFlag = false;
    var _manualSeekThreshold = -500;
    var inactivityExceeded = false;
    var volume = 100;
    var isOpeningState = false;
    var liveStartTime, liveStartOffset = 0;
    var _isSeekingToEnd = false;
    var _playCount = 0;
    var _hasReachedLiveEnd = false;
    var _liveStartTimeWasSetAt = 0;
    var _zeroEquivalencyThreshold = (1 / 5) * 1000 * 1.05; // 5fps + 5% slop = worst case.
    var currentPlaybackRate = 1.0;
    var canFullScreen, enterFullScreen;
    var liveMediaResetInProgress = false;
    var lastReportedVideoLevel = -1;
    var lastReportedAudioLevel = -1;
    var positionWhenPlayWasCalled = 0;
    var suppressPositionUpdatedUntilPlaying = false;
    var isEndedState = false;

    var uniquifyCounter = 0; // The number of times we've attempted to uniquify the media URL.
    var maxUniquifications = 4; // The maximum allowed attempts to uniquify the media URL. 

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

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

    var liveMediaOpenReconnector = new Mediasite.MediaPlayer.LiveMediaOpenReconnector();

    var convivaConfiguration = makeConvivaConfig(viewOptions);
    var convivaTracker = Mediasite.Player.Conviva.createConvivaTracker();
    convivaTracker.configure(convivaConfiguration);

    var _events = new Mediasite.Player.EventBundle();
    var _eventNames = Mediasite.MediaPlayer.ModelEvent;
    var mediaPlayerView;
    var state;
    var mediaWrapper;
    var dashPlayer;

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

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

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

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

    function isLivePresentation() {
        return !!liveStartTime;
    }

    function detectFullScreen(videoElement) {
        /// <summary>Define the appropriate fullscreen functionality for a given browser, if available</summary>

        // This should not be checked until the media is ready (and any on-demand/live settings have already been applied), 
        // since that affects whether fullscreen functionality is enabled

        logger.Debug("Detecting fullscreen functionality...");
        canFullScreen = false; // Default state

        // TODO: see if IE9 ever exposes its own method
        if (typeof videoElement.requestFullScreen === 'function') {
            // W3C standard, Blink browsers


            enterFullScreen = function () {
                try {
                    videoElement.requestFullScreen();
                    return true;
                } catch (err) {
                    logger.Error("Fullscreen error", err);
                }
            };

            //isFullScreen = function () {
            //  use fullscreenchange event instead
            //};
            // }
        }
        else if (typeof videoElement.webkitEnterFullScreen === 'function') {
            // (Older) Webkit browsers

            if (isLivePresentation() && Mediasite.BrowserDetect.Android()) {
                // Older Android has issues resuming live streams after returning from fullscreen (in Mediasite players)

                logger.Debug("... not supporting fullscreen on Android for live stream");
            } else {

                // Safari, Mobile Safari, Android (on-demand)
                enterFullScreen = function () {
                    try {
                        videoElement.webkitEnterFullScreen();
                        return true;
                    } catch (err) {
                        logger.Error("Fullscreen error", err);
                    }
                };

                //isFullScreen = function () {
                //  return videoElement.webkitDisplayingFullscreen;
                //};
            }
        } else if (typeof videoElement.mozFullScreen === 'function') {
            // Mozilla browsers
            enterFullScreen = function () {
                try {
                    document.mozRequestFullScreen();
                    return true;
                } catch (err) {
                    logger.Error("Fullscreen error", err);
                }
            };

            //isFullScreen = function () {
            //    return document.mozFullScreen;
            //};
        }


        if (typeof enterFullScreen == "function") { // && typeof isFullScreen == "function") {
            canFullScreen = true;
        }

        logger.Debug("... can fullscreen: " + canFullScreen, enterFullScreen && enterFullScreen.toString());
    }

    function resetLiveMediaElement() {
        liveMediaResetInProgress = true;

        // Remember audio values from before reset.
        var volumeBefore = self.GetVolume();
        var mutedBefore = self.IsMuted();

        dashPlayer.reset();
        dashPlayer.attachView(mediaElement);
        dashPlayer.attachSource(lastLoadedMediaSource);

        // Restore the pre-reset audio values.
        self.SetVolume(volumeBefore);
        if (mutedBefore) {
            self.Mute();
        }

        if (disableVolume) {
            self.SetVolume(0);
        }
    }

    function reconnectOnLiveMediaOpenFailure() {
        resetLiveMediaElement();
    }

    function raiseMediaError(errorDescription) {
        convivaTracker.handleMediaError(errorDescription, true);

        _events.fire(_eventNames.MediaError, errorDescription);
    }

    function updateBitrate (bitrateBps) {
        convivaTracker.handleBitrateChange(bitrateBps);
    }

    function handleDashPlaybackError(e) {
        if (liveMediaOpenReconnector.HandleLiveMediaOpenFailure(reconnectOnLiveMediaOpenFailure)) {
            return; // The error is being handled by the LiveMediaOpenReconnector.
        }

        logger.Error('DASH.JS ERROR: PLAYBACK_ERROR');
        raiseMediaError(Mediasite.Player.Localization.MediaPlayer.GeneralError);
    }

    function handleDashError(e) {
        
        if (liveMediaOpenReconnector.HandleLiveMediaOpenFailure(reconnectOnLiveMediaOpenFailure)) {
            return; // The error is being handled by the LiveMediaOpenReconnector.
        }

        var errorDescription;

        if (e.error === 'download') {
            errorDescription = Mediasite.Player.Localization.MediaPlayer.AdaptiveStreamingNetworkError;
        } else {
            errorDescription = Mediasite.Player.Localization.MediaPlayer.GeneralError;
        }

        logger.Error('DASH.JS ERROR  (' + e.error + ')');
        raiseMediaError(errorDescription);
    }


    var cannotLoadDelay = 10000;
    function handleCannotLoadMedia() {

        if (mediaElement && mediaElement.networkState == 3) {  // NETWORK_NO_SOURCE
            logger.Error("Trouble loading media file, file or server is probably unavailable");
            raiseMediaError(Mediasite.Player.Localization.MediaPlayer.SilverlightNetworkError);
        }
    }

    function reportStateChanged(newState) {
        state = newState;
        _events.fire(_eventNames.StateChanged, newState);
    }


    var nudged;
    function handleMediaEvent(event) {

        var ev = dashjs.MediaPlayer.events;

        if (event.type === "loadstart") {
            isOpeningState = true;
            mediaPlayerView.MediaLoadStart();
            reportStateChanged(Mediasite.Player.MediaState.Opening);
            _events.fire(_eventNames.MediaLoadStart);
            //setTimeout(handleCannotLoadMedia, cannotLoadDelay);
        }

        if (event.type === 'stalled' && Mediasite.BrowserDetect.Chrome() && !Mediasite.BrowserDetect.ChromeAndroid() && mediaElement && mediaElement.readyState === mediaElement.HAVE_NOTHING) {
            // 
            // Fix for BUG37589 (improved to be a bit more cache/CDN friendly).
            // 
            // Google Chrome can struggle to load the same source URL in two 
            // video elements at the same time, even when the video elements 
            // live in separate windows. While the video is downloading for 
            // one request, the cache item is locked for the second request. 
            // This impacts player pop-outs, stream pop-outs, and just opening 
            // multiple copies of the same presentation.
            // 
            // See https://code.google.com/p/chromium/issues/detail?id=31014
            // 
            // Chrome now blocks the second request for twenty seconds before 
            // giving up and requesting a second copy of the video file. 
            // Twenty seconds is way too long to wait, especially since it 
            // will be an additional twenty seconds each time the user seeks 
            // (while the first video request is still active).
            // 
            // This fix attempts to detect when a video request is blocked and 
            // re-request the video with a different URL. When cache blocked 
            // as described, the video element raises a 'stalled' event while 
            // the element's readyState is still equal to HAVE_NOTHING. When 
            // that condition is detected, the video URL is modified in such a 
            // way that the browser will treat it as a different asset yet  
            // the media server will respond with the same video file.
            // 
            // Making the video URLs truly unique can prevent beneficial 
            // caching and can result in excessive bandwidth usage and server 
            // load. For this reason, the video URLs are modified such that 
            // there will only exist a finite number of variations for a given 
            // video asset.
            // 

            var mediaType = viewOptions.pluginMimeType;
            var mediaSource = selectMediaSourceByMimeType(mediaSources, mediaType);

            if (canUniquifyMediaUrl(mediaSource)) {
                mediaSource = uniquifyMediaUrl(mediaSource);
                setVideoElementSource(element, mediaSource, mediaType);
                return;
            }
        }


        if (isOpeningState && (event.type === ev.CAN_PLAY || event.type === "suspend" || event.type === "stalled")) {
            // Invalidate the Opening state when we can play or have encountered 
            // suspend or stalled. This is intended to prevent the player from 
            // getting stuck showing "Opening" when it's really not doing so.

            if (viewOptions.isMobile) {
                state = Mediasite.Player.MediaState.Ready;
                reportStateChanged(state);
            }

            if (Mediasite.BrowserDetect.Android()) {
                // Video may be ready to load and start autoplaying, but some browsers (Dolphin) need a nudge
                // to continue after switching video sources.

                clearTimeout(nudged);
                nudged = setTimeout(function () {
                    mediaElement.play();
                }, 200);
            }

            isOpeningState = false;
        }

        var metadataUpdated = false;
        if (event.type === "loadeddata" || event.type === "durationchange") {
            metadataUpdated = didMediaMetadataUpdate();
        }

        if (event.type === "loadeddata"
            || (event.type === ev.CAN_PLAY && Mediasite.BrowserDetect.IOS6())
            || (event.type === "durationchange" && Mediasite.BrowserDetect.Android())) {

            // In desktop browsers, this fires when the video loads on pageload and is a reliable indicator that the media metadata is available.
            // On mobile devices, this fires several times during pageload, but should only be acted on when the media is actually loaded, after user initiates playing.
            // On iOS 6, when loading a new URL in an existing media element, "loadeddata" and "canplay" fire several times, but media metadata seems to be only available after several "canplay" events.
            //
            // To indicate if media's metadata is available, check if the media has a plausible duration.
            // 
            // Before properly loading the media, various browsers report various values for media.duration:
            //      Android:       video.duration = 1
            //      Chrome Android:               = 100
            //      Blackberry:                   = Infinity

            // Do initial setup if video is available
            var isLive = isLivePresentation();
            var mediaIsReady = false;
            if (isLive) {
                mediaIsReady = true;
            } else if (mediaElement.duration > 1 && mediaElement.duration != 100 && mediaElement.duration != Infinity) {
                mediaIsReady = true;
            }

            logger.Debug("Attempting to load media metadata.",
                "Live status? " + (isLive ? "live" : "on-demand"),
                "Media? " + (mediaIsReady ? "ready" : "not ready"),
                "media duration: " + mediaElement.duration);

            if ((metadataUpdated || !isSetUp) && mediaIsReady) {

                var mediaInfo = getMediaInfo(mediaElement);
                mediaPlayerView.UpdateMediaInfo(mediaInfo);

                if (!isSetUp) {
                    isSetUp = true;

                    self.SetVolume(volume);

                    //BUG36830 - MSL
                    //Special casing for ios6
                    //ios6 must wait for progress event to fire before
                    //we can set the initial start position and begin playing
                    //this is needed for the playFrom querystring parameter and
                    //also the Play As Video swaping function
                    var ios6 = Mediasite.BrowserDetect.IOS6();
                    if (ios6 && !isLive) {
                        element.bind('progress', function () {
                            element.unbind('progress');
                            _events.fire(_eventNames.MediaLoaded, mediaInfo);
                        });
                    } else {
                        _events.fire(_eventNames.MediaLoaded, mediaInfo);
                    }
                }
            }
        }


        if (event.type === "durationchange" && Mediasite.BrowserDetect.Android()) {
            if (isSetUp && metadataUpdated) {
                var mediaInfo = getMediaInfo();
                _events.fire(_eventNames.MediaUpdated, mediaInfo);
            }
        }

        switch (event.type) {
            case ev.CAN_PLAY:
                if (liveMediaResetInProgress) {
                    dashPlayer.play();
                    liveMediaResetInProgress = false;
                }
                break;
            case "volumechange":
                var vol = 0;
                if (!mediaElement.muted) {
                    vol = Math.round(self.GetVolume());
                }
                _events.fire(_eventNames.VolumeChanged, vol);
                break;
            case ev.PLAYBACK_RATE_CHANGED:
                var playbackRateInfo = {
                    Min: playbackRates[0],
                    Max: playbackRates[playbackRates.length - 1],
                    Current: Math.round(self.GetPlaybackRate() * 100) / 100,
                    Rates: $.extend([], playbackRates)
                };

                logger.Debug("Playback rate now " + playbackRateInfo.Current);
                _events.fire(_eventNames.PlaybackRateChanged, playbackRateInfo);
                break;
            case ev.PLAYBACK_PLAYING:
                mediaElement.playbackRate = currentPlaybackRate;
                liveMediaOpenReconnector.setMediaState(Mediasite.Player.MediaState.Playing);
                reportStateChanged(Mediasite.Player.MediaState.Playing);
                
                // When playing begins, we expect a PositionChanged event that 
                // matches the time we were resting at when the play() was called.
                // 
                // variables `positionWhenPlayWasCalled` and `suppressPositionUpdatedUntilPlaying` 
                // help accomplish this.
                if (suppressPositionUpdatedUntilPlaying) {
                    _events.fire(_eventNames.PositionChanged, positionWhenPlayWasCalled, true);
                    suppressPositionUpdatedUntilPlaying = false;
                }

                _isSeekingToEnd = false;

                _playCount++;

                break;
            case ev.PLAYBACK_PAUSED:
                _events.fire(_eventNames.PositionChanged, self.GetPosition());
                if (isLivePresentation()) {
                    reportStateChanged(Mediasite.Player.MediaState.Stopped);
                } else {
                    reportStateChanged(Mediasite.Player.MediaState.Paused);
                }
                break;
            case ev.PLAYBACK_SEEKING:
                _events.fire(_eventNames.SeekStarting);
                // Don't report current position at this point
                // because the media element may end up at a different position
                break;
            case ev.PLAYBACK_SEEKED:
                _events.fire(_eventNames.SeekEnding);
                _isSeekingToEnd = false;
                if (manualSeekFlag) {
                    manualSeekFlag = false;
                }
                else {
                    lastSeekTime = self.GetPosition();
                }
                break;
            case ev.PLAYBACK_TIME_UPDATED:
                if (_isSeekingToEnd) {
                    // We don't want to update our current position until 
                    // we've settled on where we want to play from. 
                    // (particularly when seeking ahead and coming up short)
                    return;
                }

                var currentPosition = self.GetPosition();

                //ios 7 seeking fix - MSL
                //We noticed that in IOS 7.0.2 & 7.0.3 that after seeking, the video element
                //would occasionally report a few timeupdates with times previous to the manual seek.
                //This code seeks to alleviate those situations by monitoring timeupdates for 5 seconds after a seek
                //and skip and timeupdates that appear to erroneous
                if (viewOptions.isMainStream) {
                    var currentTimeDate = new Date();
                    var elapsedRealTime = currentTimeDate - lastSeekTimeDate;
                    var elapsedVideoTime = elapsedRealTime * currentPlaybackRate;
                    var expectedTime = lastSeekTime + elapsedVideoTime;
                    var timeVariance = Math.abs(currentPosition - expectedTime);

                    if (lastSeekTime > 0 && elapsedRealTime < 5000) {
                        if (timeVariance > 5000) {
                            return;     //currentPosition does not appear to be correct and will be skipped. 
                        }
                    }
                }
                /*
                Mediasite.Debug.Repo.Push("timeupdate", {
                currentPosition: currentPosition,
                playbackRate: currentPlaybackRate,
                streamType: viewOptions.streamType,
                timestamp: Date.now()
                });
                */

                // When playing begins, we expect a PositionChanged event that 
                // matches the time we were resting at when the play() was called.
                // 
                // variables `positionWhenPlayWasCalled` and `suppressPositionUpdatedUntilPlaying` 
                // help accomplish this.
                if (!suppressPositionUpdatedUntilPlaying) {
                    _events.fire(_eventNames.PositionChanged, currentPosition, true);
                }
                break;
            case ev.PLAYBACK_ENDED:
                convivaTracker.handleMediaEnded(isLivePresentation());

                _events.fire(_eventNames.PositionChanged, self.GetPosition());

                self.Pause();
                isEndedState = true;
                _events.fire(_eventNames.PositionChanged, 0);
                reportStateChanged(Mediasite.Player.MediaState.MediaEnded);
                break;
        }
    }


    var didMediaMetadataUpdate = (function () {
        var lastDuration;
        var lastAspectRatio;

        return function () {
            var updated = false;

            var currentDuration = mediaElement.duration;
            var currentAspectRatio = mediaElement.videoWidth / mediaElement.videoHeight;

            updated = updated || (currentDuration != lastDuration || currentAspectRatio != lastAspectRatio);

            lastDuration = currentDuration;
            lastAspectRatio = currentAspectRatio;

            return updated;
        }
    })();

    function getMediaInfo() {
        logger.Debug("Loading media metadata ...");
        var isLive = isLivePresentation();

        var canChangeSpeed = !(isLive || Mediasite.BrowserDetect.IOS() || Mediasite.BrowserDetect.Android());

        detectFullScreen(mediaElement);

        var aspectRatio = getAspectRatio();

        var duration = self.GetDuration();

        var mediaInfo = {
            AspectRatio: aspectRatio,
            AudioOnly: false,
            Duration: duration,
            CanChangeSpeed: canChangeSpeed,
            CanFullScreen: canFullScreen,
            CurrentLanguageIndex: 0,
            Languages: {},
            StreamType: viewOptions.streamType,
            IsMainStream: viewOptions.isMainStream
        };

        logger.Debug("... finished loading media metadata.", mediaInfo);


        return mediaInfo;
    }

    function getAspectRatio() {
        var lastBitrateItem;
        var videoBitrateList = dashPlayer.getBitrateInfoListFor('video');
        var hasVideo = videoBitrateList && videoBitrateList.length > 0;

        if (!hasVideo) {
            return 0;
        }

        lastBitrateItem = videoBitrateList[videoBitrateList.length - 1];

        if (lastBitrateItem.height == 0 || lastBitrateItem.width == 0) {
            return 0;
        }


        return lastBitrateItem.width / lastBitrateItem.height;
    }


    /*
    On small devices like iPhone or iPod, in mobile Safari, video plays in fullscreen.
    When exiting full-screen, the video element reports timeupdate -> 0 even if the video was
    at time: X when the user exited fullscreen. (Observed on iOS7 iPod in mobile Safari.)
    This codeq intercedes: "belay that, the time was actually X" (except when rewinding/seeking).

    Played through end of presentation:

    [Log]   timeupdate    currentTime: [end of media]
    [Log]   ended
    [Log]   seeking
    [Log]   timeupdate    currentTime: 0
    [Log]   seeked
    [Log]   timeupdate    currentTime: 0

    mediaElement.currentTime:  0

    -- vs --

    Simply exiting fullscreen

    [Log]   timeupdate    currentTime: [arbitrary time not at end]
    [Log]   timeupdate    currentTime: 0
    [Log]   pause
    [Log]   webkitendfullscreen

    mediaElement.currentTime:  0
    */
    var fullscreenPosition;
    var handleFullscreenTimeEvents = (function () {
        var isFullscreen;
        var allowZeroPosition;

        return handleFullscreenTimeEvents;

        function handleFullscreenTimeEvents(event) {
            // console.log(event.type, getPosition())
            switch (event.type) {
                case "webkitendfullscreen":
                    // Belay the previous useless "timeupdate: 0" from the media element leaving fullscreen
                    _events.fire(_eventNames.PositionChanged, fullscreenPosition);
                    // console.log("webkitendfullscreen", "position:", fullscreenPosition);

                    isFullscreen = false;
                    break;
                case "webkitbeginfullscreen":
                    // Start tracking legitimate currentTimes
                    isFullscreen = true;
                    allowZeroPosition = false;
                    fullscreenPosition = getPosition();
                    return;
                case "seeked":
                    if (isFullscreen) {
                        // User/API seeked or media element is rewinding from the end
                        // Expect a timeupdate to follow. It's still legitimate if it's s 0.
                        allowZeroPosition = true;
                    }
                    // console.log("seeked", "allowZeroPosition:", allowZeroPosition);
                    break;
                case "timeupdate":
                    if (isFullscreen) {
                        //  Record the current position (if it's legitimate)
                        //  for when the media element inevitably betrays us


                        var position = getPosition();
                        if (position > 0 || allowZeroPosition) {
                            fullscreenPosition = position;
                        }

                        // Don't trust subsequent timeupdate events of currentTime=0
                        allowZeroPosition = false;
                    } else {
                        // Video is not fullscreen, assume timeupdate/currentTime is legitimate
                        fullscreenPosition = false;
                    }
                    break;
            }
        }
    })();


    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
        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;
    };

    this.SetupMediaElement = function (sources) {

        // Override sources parameter with the single video source that 
        // we KNOW we want to play. This is part of the ongoing refactoring 
        // of the media player loading code.
        sources = [viewOptions.selectedVideoSource];

        var msSinceLiveStartWasSet = (new Date()).getTime() - _liveStartTimeWasSetAt;
        var isTransitionFromWaitingToLive = msSinceLiveStartWasSet < 2 * 1000;

        mediaPlayerView = $.extend(new Mediasite.MediaPlayer.BaseView(), new Mediasite.MediaPlayer.Html5View(viewOptions));
        element = mediaPlayerView.CreateMediaElement();
        mediaWrapper = mediaPlayerView.Wrapper;
        mediaElement = element.get(0);
        mediaSources = sources;

        // Android allegedly works better if preload=none; autoplay overrides preload. (Android shouldn't autoplay anyway.)
        if (Mediasite.BrowserDetect.Android()) {
            mediaElement.setAttribute('preload', 'none');
        }

        // Set up event handling
        if (Mediasite.BrowserDetect.IOS()) {
            element.bind("webkitbeginfullscreen webkitendfullscreen seeked timeupdate", handleFullscreenTimeEvents);
        }
        element.bind("stalled canplaythrough emptied suspend waiting abort loadstart play loadeddata durationchange volumechange", handleMediaEvent);
        element.bind('playCoverPressed', function () { self.Play(); });
        element.bind('contextmenu', function () { return false; });

        

        // Determine which media source to use
        var mediaSource;
        var mediaType;
        for (var i = 0; i < sources.length; i++) {
            if (sources[i].MimeType === viewOptions.pluginMimeType) {
                mediaSource = (sources[i].Location);
                mediaType = sources[i].MimeType;
                break;
            }
        }

        // Sanity check media source
        if (typeof (mediaSource) === 'undefined') {
            _events.fire(_eventNames.MediaError, Mediasite.Player.Localization.MediaPlayer.NoCompatibleMediaError);
            return;
        }


        if (isTransitionFromWaitingToLive) {
            touchThePublishingPoint(mediaSource, mediaType, function () {
                setVideoElementSource(element, mediaSource, mediaType);
            });
        } else {
            setVideoElementSource(element, mediaSource, mediaType);
        }

        detectFullScreen(mediaElement); // try to see if fullscreen support might be available in order to provide UI which anticipates this functionality
    };

    this.SetNewVideo = function (sources, streamType) {

        //Change the video url for the Play As Video function

        var oldVideoSource = viewOptions.selectedVideoSource;
        var newVideoSource = findMediaSource(sources, oldVideoSource.MediaType, oldVideoSource.MimeType, oldVideoSource.ServerType);

        if (!newVideoSource || !newVideoSource.Location) {
            _events.fire(_eventNames.MediaError, Mediasite.Player.Localization.MediaPlayer.NoCompatibleMediaError);
            return;
        }

        // Determine which media source to use
        var mediaType = viewOptions.pluginMimeType;
        var mediaSource = newVideoSource.Location;

        if (element == undefined) {
            _events.fire(_eventNames.MediaError, Mediasite.Player.Localization.MediaPlayer.NoCompatibleMediaError);
            return;
        }

        viewOptions.streamType = streamType;
        viewOptions.selectedVideoSource = newVideoSource;
        mediaSources = sources;

        setVideoElementSource(element, mediaSource, mediaType);

        isSetUp = false;
    }

    function findMediaSource(sources, mediaType, mimeType, serverType) {
        var i, s;
        for (i = 0; i < sources.length; i += 1) {
            var s = sources[i];
            if (s.MediaType === mediaType && s.MimeType === mimeType && s.ServerType === serverType) {
                return s;
            }
        }
    }

    function selectMediaSourceByMimeType(sources, mimeType) {
        for (var i = 0; i < sources.length; i++) {
            if (sources[i].MimeType === mimeType) {
                return (sources[i].Location);
            }
        }
    }

    function bitrateForTypeAndLevel(mediaType, levelIndex) {
        var bitrateInfos;

        if (!dashPlayer || !dashPlayer.getBitrateInfoListFor) {
            return 0;
        }

        bitrateInfos = dashPlayer.getBitrateInfoListFor(mediaType);

        if (levelIndex < 0 || levelIndex > bitrateInfos.length - 1) {
            return 0;
        }

        return bitrateInfos[levelIndex].bitrate;
    }

    function checkForBitrateChange() {
        var videoLevel = -1;
        var audioLevel = -1;
        var videoBitrate = 0;
        var audioBitrate = 0;
        var totalBitrate = 0;

        if (!dashPlayer || !dashPlayer.getQualityFor) {
            return;
        }

        videoLevel = dashPlayer.getQualityFor('video');
        audioLevel = dashPlayer.getQualityFor('audio');

        if (dashPlayer && dashPlayer.isPaused()) {
            videoLevel = -1;
            audioLevel = -1;
        }

        if (lastReportedVideoLevel != videoLevel || lastReportedAudioLevel != audioLevel) {
            lastReportedVideoLevel = videoLevel;
            lastReportedAudioLevel = audioLevel;
            videoBitrate = bitrateForTypeAndLevel('video', videoLevel);
            audioBitrate = bitrateForTypeAndLevel('audio', audioLevel);
            totalBitrate = videoBitrate + audioBitrate;
            updateBitrate(totalBitrate);
            logger.Info('Media quality changed. -- v:' + videoLevel + ' + a:' + audioLevel + ' (bitrate v:' + videoBitrate + ' + a:' + audioBitrate + ' = ' + totalBitrate + ')');
        }
    }

    function setVideoElementSource(element, mediaSource, mediaType) {
        convivaTracker.handleMediaSourceLoading(mediaSource, mediaElement);

        self.setVideoElementSourceFunction(element, mediaSource, mediaType);

        lastLoadedMediaSource = mediaSource;
        logger.Info("Media element source added", mediaType, mediaSource);
    }

    this.setVideoElementSourceFunction = function (element, mediaSource, mediaType) {
        var video = element.get(0);
        dashPlayer = dashjs.MediaPlayer().create();
        dashPlayer.setFastSwitchEnabled(true);

        // "stalled canplaythrough emptied suspend waiting abort loadstart play loadeddata durationchange volumechange"
        
        var ev = dashjs.MediaPlayer.events;

        var dashPlayerMediaEvents = [ev.BUFFER_EMPTY, ev.CAN_PLAY, ev.ERROR, ev.PLAYBACK_METADATA_LOADED,
            ev.PLAYBACK_PLAYING, ev.PLAYBACK_PAUSED, ev.PLAYBACK_ENDED, ev.PLAYBACK_SEEKING,
            ev.PLAYBACK_SEEKED, ev.PLAYBACK_RATE_CHANGED, ev.PLAYBACK_TIME_UPDATED];

        var i;
        for (i = 0; i < dashPlayerMediaEvents.length; i += 1) {
            dashPlayer.on(dashPlayerMediaEvents[i], handleMediaEvent);
        }

        dashPlayer.on(ev.ERROR, handleDashError);
        dashPlayer.on(ev.PLAYBACK_ERROR, handleDashPlaybackError);

        element.off('timeupdate', checkForBitrateChange);
        element.on('timeupdate', checkForBitrateChange);

        dashPlayer.getDebug().setLogToBrowserConsole(false);
        dashPlayer.initialize(video, mediaSource, false);
    };

    function makeConvivaConfig(options) {

        var lateBoundConvivaConfigs = {
            streamType: options.streamType,
            pluginMimeType: options.pluginMimeType,
            streamUrl: viewOptions.selectedVideoSource.Location,
            mediaPlayerType: options.mediaPlayerType
        }

        return options.getConvivaConfiguration(lateBoundConvivaConfigs);
    }

    function canUniquifyMediaUrl(mediaUrl) {
        // TODO: Keep separate counters for each mediaUrl.

        return uniquifyCounter < maxUniquifications;
    }

    function uniquifyMediaUrl(mediaUrl) {
        var paramKey = 'sf-uniquify';
        var combiner = (mediaUrl.indexOf('?') === -1) ? '?' : '&';
        var value;

        uniquifyCounter += 1;

        if (uniquifyCounter > maxUniquifications) {
            throw { name: 'ExcessiveMediaUrlUniquifications', message: 'The maximum number of media URL uniquification attempts has been exceeded.' };
        }

        value = uniquifyCounter;

        return mediaUrl + combiner + paramKey + '=' + value;
    }

    function touchThePublishingPoint(mediaSource, mediaType, callback) {
        logger.Debug('Kicking the HLS publishing point to make sure it is awake.');
        var $vid = $('<video webkit-playsinline playsinline></video>');
        var vid = $vid.get(0);
        setVideoElementSource($vid, mediaSource, mediaType);
        vid.load();

        window.setTimeout(function () {
            vid.pause();
            $vid.remove();
            $vid = null;
            vid = null;

            callback();
        }, 1000);
    }






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

        // When we get the UpdateOptions call, we know that the 
        // controller is ready for our notifications. This is the 
        // closest thing we have to a Ready or Initialized method.

        // Dash is always significantly delayed, so we can be lazy 
        // about transitioning from the waiting to live states. 
        // If we're too eager to load an Dash stream, we can 
        // attempt to load before the manifest/publishing point is ready, 
        // causing the video element to get into an unrecoverable 
        // non-loaded state.
        _events.fire(_eventNames.EnableLiveStartDelay, liveStartUnsafeSeconds * 1000);
    };

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

        if (isLively) {
            _events.fire(_eventNames.EnableLiveJumpBackDetection);
        }

        liveMediaOpenReconnector.setLiveStatus(status);
        if (status == ps.LiveEnded) {
            convivaTracker.handleMediaEnded(isLivePresentation());

            _hasReachedLiveEnd = true;
            _events.fire(_eventNames.PositionChanged, 0);
            //self.Stop();
        }
    };

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

        if (status === Mediasite.Player.PresentationStatus.Live || status === Mediasite.Player.PresentationStatus.LiveEnded) {
            window.location.reload();
        }
    };

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

        var positionInSeconds = positionInMS / 1000;

        manualSeekFlag = true;
        lastSeekTime = positionInMS;
        lastSeekTimeDate = new Date();

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

            // prevent currentTime from being set to the same value multiple times in a row - BUG41168
            if (dashPlayer.time() != positionInSeconds) {
                isEndedState = false;
                dashPlayer.seek(positionInSeconds);

                // preemptively report the requested position as the "current" position and assume the media element will catch up soon
                _events.fire(_eventNames.PositionChanged, positionInMS);
            }

        }
    };

    this.PlayDefaultRate = function () {
        var playbackRate = defaultPlaybackRate;
        self.SetPlaybackRate(playbackRate);
    };

    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 () {
        if (state === Mediasite.Player.MediaState.Ready) {
            // Media player must be activated before full screen can be activated
            self.Play();
        }

        if (!canFullScreen) {
            // Double-check fullscreen capability, since we might have been asked
            // to fullscreen before running the usual capability check.
            detectFullScreen(mediaElement);
        }


        if (canFullScreen && typeof enterFullScreen == "function") {
            enterFullScreen();
        }
    };

    this.SetAudioLanguage = function (language) {
    };

    this.Pause = function () {
        if (isLivePresentation()) {
            self.Stop();
        } else {
            dashPlayer.pause();
            reportStateChanged(Mediasite.Player.MediaState.Paused);
        }
    };

    this.Play = function () {

        if (inactivityExceeded === true) {
            window.location.reload();
            return;
        }

        if (liveMediaResetInProgress) {
            return;
        }

        if (isLivePresentation() && _playCount > 0) {
            // Most browsers play live Dash from the paused position 
            // when resuming play. This can cause the video to lag (significantly) 
            // behind live. Destroy and recreate the media element to avoid issues.
            // This approach does not work well on clients that require user interaction 
            // on first play, since the user will have to press play again.

            resetLiveMediaElement();
            return;
        }

        if (isEndedState) {
            self.SetPosition(0);
            isEndedState = false;
        }

        // When playing begins, we expect a PositionChanged event that 
        // matches the time we were resting at when the play() was called.
        // 
        // variables `positionWhenPlayWasCalled` and `suppressPositionUpdatedUntilPlaying` 
        // help accomplish this.
        positionWhenPlayWasCalled = self.GetPosition();
        if (state !== Mediasite.Player.MediaState.Playing) {
            suppressPositionUpdatedUntilPlaying = true;
        }
        _events.fire(_eventNames.PositionChanged, positionWhenPlayWasCalled, true);

        // Remember the pre-play playback rate.
        var rateBeforePlay = self.GetPlaybackRate();
        dashPlayer.play();

        if (self.GetPlaybackRate() != rateBeforePlay) {
            // Restore the pre-play rate.
            // IE9 sets the rate to 1.0 whenever play() is called, 
            // but the correct behavior for our player is to maintain 
            // playback rate. Silly IE! Note that if the user 
            // right-clicks on the video and chooses "Play" from the 
            // context menu, the playback rate will still be set to 1.0.
            self.SetPlaybackRate(rateBeforePlay);
        }
    };

    this.Stop = function () {
        dashPlayer.pause();

        reportStateChanged(Mediasite.Player.MediaState.Stopped);
    };

    this.FinalizeLive = function () {
        self.Stop();

        dashPlayer.reset();
        dashPlayer.attachView(mediaElement);
    };

    function getPosition() {
        if (isLivePresentation()) {
            return mediaElement.currentTime * 1000;
        } else {
            return dashPlayer.time() * 1000 + liveStartOffset;
        }
    }
    this.GetPosition = function () {
        var position = getPosition();

        if (fullscreenPosition && position === 0) {
            // Don't trust the media element, use the latest legitimate position
            position = fullscreenPosition;
        }


        if (position < _zeroEquivalencyThreshold && lastSeekTime == 0) {
            // In iOS, seeking back to zero may cause the video element to 
            // come to rest at a position slightly greater than zero. This 
            // seems to occur when the MP4 content was transcoded from 
            // a Smooth Streaming source using the MATCH SOURCE option. 
            // The position at which the video element rests is equal to 
            // the time between frames in the video. For example, for 25fps 
            // content, the position is 40ms. To work around this behavior, 
            // we will return zero from positions very close to zero.
            return 0;
        }

        if (_hasReachedLiveEnd) {
            // Don't listen to that silly media element. 
            // If live has ended, we're at zero.
            return 0;
        }

        if (lastSeekTime) {
            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;
            }
        }

        return position;
    };

    this.GetDuration = function () {
        if (isLivePresentation()) {
        return 0;
        } else if (dashPlayer.duration() == Number.POSITIVE_INFINITY) {
            return 0;
        }
        return dashPlayer.duration() * 1000;
    };

    this.SetVolume = function (val) {
        if (disableVolume) {
            dashPlayer.setVolume(0);
            dashPlayer.setMute(true);
        } else {
            dashPlayer.setVolume(val / 100);
            dashPlayer.setMute(val == 0);
            _events.fire(_eventNames.VolumeChanged, Math.round(self.GetVolume()));
        }
    };

    this.GetVolume = function () {
        return dashPlayer.getVolume() * 100;
    };

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

    this.StepVolumeDown = function () {
        if (!disableVolume) {
            if (dashPlayer.getVolume() > 0.2) {
                this.SetVolume((dashPlayer.getVolume() - 0.2) * 100);
            } else {
                this.SetVolume(0);
            }
        }
    };

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

    this.Mute = function () {
        dashPlayer.setMute(true);

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

    this.UnMute = function () {
        if (!disableVolume) {
            dashPlayer.setMute(false);

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

    this.IsMuted = function () {
        return dashPlayer.isMuted();
    };

    this.TogglePlayPause = function () {
        // In iOS 4.3 mediaElement.ended is always true after about the first three 
        // seconds of live HLS content, so only trust that during on-demand.
        if (dashPlayer.isPaused() || (!isLivePresentation() && mediaElement.ended)) { // dashPlayer.isEnded() doesn't exist.
            self.Play();
        }
        else {
            self.Pause();
        }
    };

    this.SetPlaybackRate = function (speed) {
        currentPlaybackRate = speed;

        if (mediaElement) {
            mediaElement.playbackRate = speed;
        }
    };

    this.GetPlaybackRate = function () {
        return mediaElement.playbackRate;
    };

    this.GetVideoWidth = function () {
        return mediaElement.videoWidth;
    };

    this.SetLiveStartTime = function (time) {
        liveStartTime = time;
        _liveStartTimeWasSetAt = (new Date()).getTime();
        liveMediaOpenReconnector.setLiveStartTime(time);
    };

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


    function SafariHlsBadStartCorrector(resetFunction) {

        // Mac Safari on OS X 10.6 can fail to start live HLS 
        // playback if connecting at the beginning of the live stream. 
        // In this case the media element raises the loadstart event, 
        // but no events after that until we intervene.


        var _resetFunction = resetFunction;
        var _detectionIntervals = [5 * 1000, 10 * 1000, 20 * 1000]; // Three attempts with progressively longer delays.
        var _correctionAttempts = 0;
        var _timer = null;
        var _enabled = true;

        this.noteMediaEvent = function (eventType) {
            if (eventType == "canplay") {
                // If we see the canplay event, we're safely past the 
                // Safari HSL false start issue. Stop all detection.

                if (correctionIsScheduled()) {
                    cancelScheduledCorrection();
                }

                _enabled = false;

            } else if (eventType == "loadstart" && shouldScheduleCorrection()) {
                scheduleNextCorrection();
            }
        };


        function correctionIsScheduled() {
            return !!_timer;
        }

        function cancelScheduledCorrection() {
            window.clearTimeout(_timer);
            _timer = null;
        }

        function scheduleNextCorrection() {
            var delay = _detectionIntervals[_correctionAttempts];
            _timer = window.setTimeout(timer_tick, delay);
        }

        function timer_tick() {
            _resetFunction();
            _correctionAttempts++;
            _timer = null;
        }

        function shouldScheduleCorrection() {
            return _enabled && (_correctionAttempts < _detectionIntervals.length);
        }
    }

};


