/// <reference path="3rdParty/jquery.js"/>
/// <reference path="Mediasite.Core.js"/>
/// <reference path="Mediasite.Player.Localization.js"/>
/*global Mediasite*/
Mediasite.namespace('Player');

Mediasite.Player.DisplayMessage = function (options) {

    defaultOptions =
    {
        title: "Mediasite Player",
        allowClose: false
    };

    $.extend(defaultOptions, options);
    $('#MessageDisplay').messagedisplay(defaultOptions);
};

Mediasite.Player.CloseMessage = function () {
    $('#MessageDisplay').messagedisplay('close');
};

Mediasite.Player.CreatePopup = function (url, name, width, height, scrollbars, resizeable) {
    width = Math.floor(width);
    height = Math.floor(height);

    var features = "width=" + width + ",height=" + height;

    if (scrollbars) {
        features += ",scrollbars=yes";
    }
    else {
        features += ",scrollbars=no";
    }

    if (resizeable) {
        features += ",resizable=yes";
    }
    else {
        features += ",resizable=no";
    }

    var popup = window.open(url, name, features);

    return popup;
};

Mediasite.Player.AppPaths =
{
    PlayerView: 'Javascript/Players/{0}/PlayerView.js',  // player name
    StyleSheet: 'Javascript/Players/{0}/{1}?version={2}',  // player name, file name, version
    ThemedStyleSheet: 'Javascript/Players/{0}/Themes/{1}/{2}?version={3}'  // player name, theme id, filename, version
};


Mediasite.Player.Register = function (playerType, playerOptions) {

    if (Mediasite.Player.Register.Players[playerType] == undefined) {
        Mediasite.Player.Register.Players[playerType] = playerOptions;
    }
};
Mediasite.Player.Register.Players = {};

Mediasite.Player.RegisterExtension = function (extensionType, extensionOptions) {

    if (Mediasite.Player.Register.Extensions[extensionType] == undefined) {
        Mediasite.Player.Register.Extensions[extensionType] = extensionOptions;
    }
};
Mediasite.Player.Register.Extensions = {};

Mediasite.Player.PlayerState = {
    NotReady: 'NotReady',
    Waiting: 'Waiting',
    InteractionRequired: 'InteractionRequired',
    Playing: 'Playing',
    Paused: 'Paused',
    Stopped: 'Stopped',
    Ended: 'Ended',
    Error: 'Error'
};

Mediasite.Player.PlayerType = {
    None: 'None',
    NewStandardPlayer: 'NewStandardPlayer',
    PipPlayer: 'PipPlayer',
    MiniPlayer: 'MiniPlayer',
    StandardPlayer: 'StandardPlayer',
    ScreenReader: 'ScreenReader',
    ContentOnlyPlayer: 'ContentOnlyPlayer',
    SmartPlayer: 'SmartPlayer'
};

Mediasite.Player.ContentCapability = {
    VideoAndSlides: 0,
    MultiVideo: 1
};

Mediasite.Player.PlayerPresentationStatus = {
    None: 0,
    Viewable: 1,
    NotFound: 2,
    Offline: 3,
    Scheduled: 4,
    NotYetAvailable: 5,
    NotAuthorized: 6
};

Mediasite.Player.PresentationStatus = {
    NotAvailable: "NotAvailable",
    ScheduledForLive: "ScheduledForLive",
    OpenForLive: "OpenForLive",
    Live: "Live",
    LivePaused: "LivePaused",
    LiveEnded: "LiveEnded",
    OnDemand: "OnDemand"
};

Mediasite.Player.PopOutPlayer = {
    Disabled: 'False',
    FullExperience: 'FullExperienceLayout',
    Automatic: 'Automatic'
};

Mediasite.Player.SmartPopOutBehavior = {
    Embedded: 'Embedded',
    Never: 'Never',
    Always: 'Always'
};

Mediasite.Player.MediaPlayerType = {
    Unknown: 0,
    WindowsMedia: 1,
    Port25: 2,
    Silverlight: 3,
    Flash: 4,
    HTML5: 5,
    HTML5Dash: 6,
    RampHLS: 7,
    HLSjs: 8,

    GetDisplayName: function (type) {
        switch (type) {
            case this.WindowsMedia:
                return "WindowsMedia";
            case this.Port25:
                return "Port25";
            case this.Silverlight:
                return "Silverlight";
            case this.Flash:
                return "Flash";
            case this.HTML5:
                return "Html5";
            case this.HLSjs:
                return "HLSjs";
            case this.HTML5Dash:
                return "Html5Dash";
            case this.RampHLS:
                return "RampHLS";
            case this.Unknown:
                return "Unknown";
        }
    },

    Parse: function (name) {
        if (typeof (name) === 'undefined') {
            return this.Unknown;
        }
        switch (name.toLowerCase()) {
            case "windowsmedia":
                return this.WindowsMedia;
            case "port25":
                return this.Port25;
            case "html5":
                return this.HTML5;
            case "hlsjs":
                return this.HLSjs;
            case "html5dash":
                return this.HTML5Dash;
            case "silverlight":
                return this.Silverlight;
            case "flash":
                return this.Flash;
            case "ramphls":
                return this.RampHLS;
            default:
                return this.Unknown;
        }
    },

    FromPluginType: function (pluginType) {
        switch (pluginType) {
            case Mediasite.Player.MediaPlugins.WindowsMedia:
                return Mediasite.Player.MediaPlayerType.WindowsMedia;
            case Mediasite.Player.MediaPlugins.Port25:
                return Mediasite.Player.MediaPlayerType.Port25;
            case Mediasite.Player.MediaPlugins.Silverlight:
                return Mediasite.Player.MediaPlayerType.Silverlight;
            case Mediasite.Player.MediaPlugins.Flash:
                return Mediasite.Player.MediaPlayerType.Flash;
            case Mediasite.Player.MediaPlugins.HTML5:
                return Mediasite.Player.MediaPlayerType.HTML5;
            case Mediasite.Player.MediaPlugins.HTML5Dash:
                return Mediasite.Player.MediaPlayerType.HTML5Dash;
            case Mediasite.Player.MediaPlugins.Hls:
                return Mediasite.Player.MediaPlayerType.HTML5;
            case Mediasite.Player.MediaPlugins.RampHls:
                return Mediasite.Player.MediaPlayerType.RampHLS;
            case Mediasite.Player.MediaPlugins.HLSjs:
                return Mediasite.Player.MediaPlayerType.HLSjs;
            default:
                return Mediasite.Player.MediaPlayerType.Unknown;
        }
    }
    
};

Mediasite.Player.TimelineEvent = {
    Unknown: 0,
    Slide: 1,
    PresentationStart: 2,
    PresentationEnd: 3,
    PauseStart: 4,
    PauseEnd: 5,
    TimedEvent: 6
};

Mediasite.Player.MediaEventType = {
    EndPresentation: "E",
    ShowSlide: "S",
    Pause: "P",
    Resume: "R"
};

Mediasite.Player.PlayerServiceFaultType = {
    Unknown: 0,
    InvalidTicket: 1
};

Mediasite.Player.MediaState = {
    Undefined: 0,
    Stopped: 1,
    Paused: 2,
    Playing: 3,
    ScanForward: 4,
    ScanReverse: 5,
    Buffering: 6,
    Waiting: 7,
    MediaEnded: 8,
    Transitioning: 9,
    Ready: 10,
    Reconnecting: 11,
    Closed: 12,
    Error: 13,
    Opening: 14,

    GetDisplayName: function (state) {
        switch (state) {
            case this.Undefined:
                return Mediasite.Player.Localization.MediaPlayer.State.Undefined;
            case this.Stopped:
                return Mediasite.Player.Localization.MediaPlayer.State.Stopped;
            case this.Paused:
                return Mediasite.Player.Localization.MediaPlayer.State.Paused;
            case this.Playing:
                return Mediasite.Player.Localization.MediaPlayer.State.Playing;
            case this.ScanForward:
                return Mediasite.Player.Localization.MediaPlayer.State.ScanForward;
            case this.ScanReverse:
                return Mediasite.Player.Localization.MediaPlayer.State.ScanReverse;
            case this.Buffering:
                return Mediasite.Player.Localization.MediaPlayer.State.Buffering;
            case this.Waiting:
                return Mediasite.Player.Localization.MediaPlayer.State.Waiting;
            case this.MediaEnded:
                return Mediasite.Player.Localization.MediaPlayer.State.MediaEnded;
            case this.Transitioning:
                return Mediasite.Player.Localization.MediaPlayer.State.Transitioning;
            case this.Ready:
                return Mediasite.Player.Localization.MediaPlayer.State.Ready;
            case this.Reconnecting:
                return Mediasite.Player.Localization.MediaPlayer.State.Reconnecting;
            case this.Closed:
                return Mediasite.Player.Localization.MediaPlayer.State.Closed;
            case this.Error:
                return Mediasite.Player.Localization.MediaPlayer.State.Error;
            case this.Opening:
                return Mediasite.Player.Localization.MediaPlayer.State.Opening;
            default:
                return Mediasite.Player.Localization.MediaPlayer.State.Unknown;
        }
    },

    GetStringValue: function (state) {
        switch (state) {
            case this.Stopped:
                return 'stopped'; break;
            case this.Paused:
                return 'paused'; break;
            case this.Playing:
                return 'playing'; break;
            case this.ScanForward:
                return 'scanforward'; break;
            case this.ScanReverse:
                return 'scanreverse'; break;
            case this.Buffering:
                return 'buffering'; break;
            case this.Waiting:
                return 'waiting'; break;
            case this.MediaEnded:
                return 'mediaended'; break;
            case this.Transitioning:
                return 'transitioning'; break;
            case this.Ready:
                return 'ready'; break;
            case this.Reconnecting:
                return 'reconnecting'; break;
            case this.Closed:
                return 'closed'; break;
            case this.Error:
                return 'error'; break;
            case this.Opening:
                return 'opening'; break;
            case this.Undefined:
            default:
                return 'undefined'; break;
        }
    }
};

Mediasite.Player.Options = {
    UserFromName: "UserFromName",
    UserFromEmail: "UserFromEmail",
    Volume: "Volume",
    SlideDisplay: "SlideDisplay",
    ShowSlideDescriptions: "ShowSlideDescriptions",
    UseScreenReader: "UseScreenReader",
    PlayerCapabilitiesError: "PlayerCapabilitiesError"
};

Mediasite.Player.MinimumFlashPlayerValues = {
    major: 10,
    minor: 1,
    computeVersion: function(major, minor) {
        return major + (minor * .1);
    },
    getMinimumVersion: function () {
        return this.computeVersion(this.major, this.minor);
    },
    convertToVersion: function (major, minor) {
        var majorInt = parseInt(major);
        var minorInt = parseInt(minor);

        return this.computeVersion(majorInt, minorInt);
    }
};

Mediasite.Player.SlideDisplayPref = {
    List: 0,
    SmallThumbs : 4,
    LargeThumbs: 5,
    Chapters: 9
};


Mediasite.Player.SearchableTimeItem = function (startTime, endTime, item) {
    this.startTime = startTime;
    this.endTime = endTime;
    this.item = item;
};

Mediasite.Player.CaptionTimeSearcher = function () {
    var self = this;
    var timeBoxSeconds = 60;

    this.matcher = new Mediasite.Player.CaptionTimeMatcher();
    this.battery = new Mediasite.Player.SearchableListBattery();

    this.addItem = function (searchableTimeItem) {
        var startCellIndex = cellIndexForTime(searchableTimeItem.startTime);
        var endCellIndex = cellIndexForTime(searchableTimeItem.endTime);
        var i;

        if (searchableTimeItem.startTime > searchableTimeItem.endTime) {
            throw {
                name: 'Invalid Argument Value',
                message: 'startTime must not be greater than endTime.'
            };
        }

        for (i = startCellIndex; i <= endCellIndex; i += 1) {
            self.battery.add(i, searchableTimeItem);
        }
    };

    this.search = function (time) {
        var cellIndex;
        
        if (time < 0) {
            return [];
        }
        
        cellIndex = cellIndexForTime(time);

        self.matcher.time = time;
        return self.battery.search(cellIndex, self.matcher.match);
    };


    function cellIndexForTime(seconds) {
        return Mediasite.Player.CaptionTimeSearcher.cellIndexForTime(timeBoxSeconds, seconds);
    }
};

Mediasite.Player.CaptionTimeSearcher.cellIndexForTime = function (cellSizeInSeconds, timeInSeconds) {
    if (cellSizeInSeconds <= 0) {
        throw {
            name: 'Invalid Argument Value',
            message: 'cellSizeInSeconds must be greater than zero.'
        };
    }

    if (timeInSeconds < 0) {
        throw {
            name: 'Invalid Argument Value',
            message: 'timeInSeconds must be greater than or equal to zero.'
        };
    }

    return Math.floor(timeInSeconds / cellSizeInSeconds);
};

Mediasite.Player.CaptionTimeMatcher = function (time) {
    var self = this;

    this.time = time || 0;

    this.match = function (captionItem) {
        return (captionItem.startTime <= self.time) && (captionItem.endTime >= self.time);
    };
};

Mediasite.Player.SearchableList = function () {
    var self = this;
    var items = [];


    this.add = function (item) {
        items.push(item);
    };

    this.remove = function (matchFunction) {
        var i;
        var numRemoved = 0;

        if (typeof matchFunction !== 'function') {
            return numRemoved;
        }

        // Iterate backward so that we can remove items without changing future indices.
        for (i = items.length - 1; i >= 0; i -= 1) {
            if (matchFunction(items[i])) {
                items.splice(i, 1);
                numRemoved += 1;
            }
        }

        return numRemoved;
    };

    this.search = function (matchFunction) {
        var results = [];
        var i;

        if (typeof matchFunction !== 'function') {
            return results;
        }

        for (i = 0; i < items.length; i += 1) {
            if (matchFunction(items[i])) {
                results.push(items[i]);
            }
        }

        return results;
    };

    this.count = function () {
        return items.length;
    };
};

Mediasite.Player.SearchableListBattery = function () {
    var cells = [];
    var self = this;

    this.createSearchableList = function () {
        return new Mediasite.Player.SearchableList();
    };


    this.count = function (cellIndex) {
        if (!cells[cellIndex]) {
            return 0;
        }

        return cells[cellIndex].count();
    };

    this.add = function (cellIndex, item) {
        if (!cells[cellIndex]) {
            cells[cellIndex] = self.createSearchableList();
        }

        cells[cellIndex].add(item);
    };

    this.remove = function (cellIndex, matchFunction) {
        if (!cells[cellIndex]) {
            return 0;
        }

        return cells[cellIndex].remove(matchFunction);
    };

    this.search = function (cellIndex, matchFunction) {
        if (!cells[cellIndex]) {
            return [];
        }

        return cells[cellIndex].search(matchFunction);
    };

}


//========================================================================================
// TimelineThing
// @param {string} id - Unique identifier for this thing.
// @param {number} time - Media time in seconds.
//========================================================================================

Mediasite.Player.TimelineThingRepository = function () {
    var self = this;
    var thingLookup = new Mediasite.Player.LookupTable();

    function timeCompare(a, b) {
        return a.time - b.time;
    }


    this.searcher = new Mediasite.Player.TimelineThingSearcher();


    this.addThing = function (timelineThing) {
        // TODO: validate timelineThing 

        thingLookup.add(timelineThing.id, timelineThing);
        self.searcher.addThing(timelineThing);
    };

    this.removeThing = function (id) {
        self.searcher.removeThing(id);
        thingLookup.remove(id);
    };

    this.getAllThings = function () {
        var array = thingLookup.getAll();
        array.sort(timeCompare);
        return array;
    };

    this.getById = function (id) {
        return thingLookup.getByKey(id);
    };

    this.searchByTime = function (lowTime, highTime) {
        var results = self.searcher.search(lowTime, highTime);
        results.sort(timeCompare);
        return results;
    };
};

Mediasite.Player.TimelineThingMonitor = function () {

    var self = this;
    var lastPlayheadInfo = new PlayheadInfo();
    var isCurrentlySeeking = false;
    var nextUpdateIsFirstAfterSeek = false;
    var mediaTimesInSeek = [];
    var wasPlayingBeforeSeek = false;
    var lastSearchedTimeRange = { low: -1, high: -1 };


    this.repository = new Mediasite.Player.TimelineThingRepository();


    function calcTimeRangeToSearch(early, late) {
        var playheadMovedBackward = early.mediaTime > late.mediaTime;
        var notPlayingBetweenUpdates = !early.isPlaying;

        if (playheadMovedBackward || notPlayingBetweenUpdates) {
            return { low: late.mediaTime, high: late.mediaTime };
        }

        return { low: early.mediaTime, high: late.mediaTime };
    }

    function isWithinLastSearchedTimeRange(time) {
        return lastSearchedTimeRange.low <= time && time <= lastSearchedTimeRange.high;
    }

    function consoleSay(message) {
        //console.log.apply(this, arguments);
    }

    function isPlayheadSittingAtZero(playheadInfo, lastPlayheadInfo) {
        return areEqualPlayheadInfos(lastPlayheadInfo, playheadInfo) && playheadInfo.mediaTime === 0 && !playheadInfo.isPlaying;
    }

    function areEqualPlayheadInfos(one, two) {
        return one.mediaTime === two.mediaTime && one.isPlaying === two.isPlaying;
    }


    // Applies new play head information.
    this.updatePlayheadInfo = function (mediaTimeInSeconds, isPlaying) {
        if (isCurrentlySeeking) {
            mediaTimesInSeek.push(mediaTimeInSeconds);
            return;
        }
        
        var i, results, timeRangeToSearch, seekedOverTimes;
        var playheadInfo = new PlayheadInfo(mediaTimeInSeconds, isPlaying);
        var playheadInfoHasChanged = !areEqualPlayheadInfos(lastPlayheadInfo, playheadInfo);

        if (nextUpdateIsFirstAfterSeek) {
            nextUpdateIsFirstAfterSeek = false;
            consoleSay('SEEKED ', lastPlayheadInfo.mediaTime.toFixed(3), ' --> ', mediaTimeInSeconds.toFixed(3));
        }

        if (!playheadInfoHasChanged) {
            return;
        }

        timeRangeToSearch = calcTimeRangeToSearch(lastPlayheadInfo, playheadInfo);

        consoleSay('THINGS ', timeRangeToSearch.low.toFixed(3), ' --> ', timeRangeToSearch.high.toFixed(3));

        results = self.repository.searchByTime(timeRangeToSearch.low, timeRangeToSearch.high);

        for (i = 0; i < results.length; i += 1) {
            
            // Prevent notification of reaching the same event two playhead updates in a row.
            // This can be caused by consecutive searched time ranges overlapping, but it's 
            // usually just that they share an endpoint time (e.g. 100-200, 200-300) and a 
            // timeline thing happens to reside at the shared time. Easiest way to prevent 
            // repeat notification in these cases is to ignore found things with times within 
            // the last searched time range.
            if (isWithinLastSearchedTimeRange(results[i].time)) {
                continue;
            }

            // TODO: Maybe wrap in try-catch or short setTimeout to prevent downstream 
            //       error from breaking our loop.
            consoleSay('  -- FOUND THING ', results[i].time, results[i].type);
            self.timelineThingReachedDelegate(results[i]);
        }


        // Prepare remembered data for next call.
        lastSearchedTimeRange = timeRangeToSearch;
        lastPlayheadInfo = playheadInfo;
    };

    this.beginSeek = function () {
        if (isCurrentlySeeking) {
            return;
        }

        wasPlayingBeforeSeek = lastPlayheadInfo.isPlaying;
        self.updatePlayheadInfo(lastPlayheadInfo.mediaTime, false);
        isCurrentlySeeking = true;
    };

    this.endSeek = function (seekedToSeconds) {
        if (!isCurrentlySeeking) {
            return;
        }

        isCurrentlySeeking = false;
        nextUpdateIsFirstAfterSeek = true;
        self.updatePlayheadInfo(seekedToSeconds, wasPlayingBeforeSeek);
    };

    // Should be replaced by consuming code.
    this.timelineThingReachedDelegate = function (timelineThing) {
        // No-op. This function should be replaced with something useful, like a function 
        // that fires an event.
    };


    // Represents the state of a player's play head.
    function PlayheadInfo(mediaTime, isPlaying) {
        this.mediaTime = mediaTime || 0;
        this.isPlaying = isPlaying || false;
    }
    // Indicates whether this PlayheadInfo instance and another instance are equivalent.
    PlayheadInfo.prototype.equals = function (other) {
        return this.time === other.time && this.isPlaying === other.isPlaying;
    };

};

Mediasite.Player.TimelineThingSearcher = function () {
    var self = this;
    var cellById = {}; // Remembers the cell index into which a thing was added.
    var idprefix = 'id--';

    this.idMatcher = new IdMatcher();
    this.timeMatcher = new TimeMatcher();
    this.battery = new Mediasite.Player.SearchableListBattery();
    this.cellIndexCalculatorFunction = Mediasite.Player.TimelineThingSearcher.cellIndexForTime;

    function idd(id) {
        return idprefix + id;
    }

    function toCellIndex(time) {
        return self.cellIndexCalculatorFunction(time);
    }


    this.addThing = function (timelineThing) {
        if (!timelineThing || typeof timelineThing !== 'object') {
            throw new Mediasite.ArgumentError(new Error('timelineThing must be an object.'));
        }

        if (typeof timelineThing.id !== 'string') {
            throw new Mediasite.ArgumentError(new Error('timelineThing.id must be a string.'));
        }

        if (typeof timelineThing.time !== 'number') {
            throw new Mediasite.ArgumentError(new Error('timelineThing.time must be a number.'));
        }

        var cellIndex = toCellIndex(timelineThing.time);

        cellById[idd(timelineThing.id)] = cellIndex;
        self.battery.add(cellIndex, timelineThing);
    };

    this.removeThing = function (id) {
        if (typeof id !== 'string') {
            throw new Mediasite.ArgumentError(new Error('id must be a String.'));
        }

        var idKey = idd(id);
        var cellIndex = cellById[idKey];

        delete cellById[idKey];

        if (typeof cellIndex !== 'number') {
            return 0;
        }

        self.idMatcher.id = id;
        return self.battery.remove(cellIndex, self.idMatcher.match);
    };

    this.search = function (lowTime, highTime) {
        var i, cellResults;
        var lowIndex, highIndex;
        var results = [];

        // Support single parameter.
        if (typeof highTime === 'undefined') {
            highTime = lowTime;
        }

        if (typeof lowTime !== 'number') {
            throw new Mediasite.ArgumentError(new Error('lowTime must be a Number.'));
        }

        if (typeof highTime !== 'number') {
            throw new Mediasite.ArgumentError(new Error('highTime must be a Number.'));
        }

        lowIndex = toCellIndex(lowTime);
        highIndex = toCellIndex(highTime);

        self.timeMatcher.lowTime = lowTime;
        self.timeMatcher.highTime = highTime;

        for (i = lowIndex; i <= highIndex; i += 1) {
            cellResults = self.battery.search(i, self.timeMatcher.match);
            Array.prototype.push.apply(results, cellResults);
        }

        return results;
    };


    function IdMatcher(id) {
        var self = this;

        this.id = id;

        this.match = function (item) {
            return !!item && item.id === self.id;
        };
    }

    function TimeMatcher(lowTime, highTime) {
        var self = this;

        this.lowTime = lowTime || -1;
        this.highTime = highTime || -1;

        this.match = function (item) {
            return !!item && self.lowTime <= item.time && item.time <= self.highTime;
        };
    }

};

Mediasite.Player.TimelineThingSearcher.cellIndexForTime = function (timeInSeconds) {
    var TIME_BOX_SECONDS = 1;

    if (timeInSeconds < 0) {
        throw {
            name: 'Invalid Argument Value',
            message: 'timeInSeconds must be greater than or equal to zero.'
        };
    }

    return Math.floor(timeInSeconds / TIME_BOX_SECONDS);
};


Mediasite.Player.TimingSearcher = function (timings) {

    this.GetCurrentItem = function (time) {
        var low = -1;
        var high = timings.length;
        var mid;

        while (high - low > 1) {
            mid = high + low >> 1;
            if (timings[mid].Time < time) {
                low = mid;
            }
            else if (timings[mid].Time > time) {
                high = mid;
            }
            else {
                return timings[mid];
            }
        }
        return timings[low];
    };

    this.UpdateTimings = function (newTimings) {
        timings = newTimings;
    };

    this.GetTimings = function () {
        return timings;
    };

    this.GetStartIndex = function (time) {

        var i = true;
        var high = timings.length;
        var low = -1;
        var mid;

        while (high - low > 1)
            if (timings[mid = high + low >> 1].Time < time) {
                low = mid;
            }
            else {
                high = mid;
            }

        return high;
    };

};


Mediasite.Player.TimedEventRepository = function (timedEvents) {

    var self = this;
    var assocArray = {};
    var lastEventsFired = {};
    var isFirstUse = true;
    var lastEventTime = 0;

    $.each(timedEvents, function (index, timedEvent) {
        assocArray[timedEvent.Id] = timedEvent;
    });

    this.TimingSearcher = new Mediasite.Player.TimingSearcher(timedEvents);

    this.AddTimedEvents = function (newTimedEvents) {

        $.each(newTimedEvents, function (index, timedEvent) {
            assocArray[timedEvent.Id] = timedEvent;
        });

        updateTimingSearcher();
    };

    this.GetAllTimedEvents = function () {
        return getArray();
    };

    function getArray() {

        var flatArray = [];

        $.each(assocArray, function (index, timedEvent) {
            flatArray.push(timedEvent);
        });

        timedEvents = flatArray;
        timedEvents.sort(compare);
        return timedEvents;
    };

    function updateTimingSearcher() {
        var flatArray = getArray();
        self.TimingSearcher.UpdateTimings(flatArray);
    }

    function compare(a, b) {
        if (a.Time < b.Time)
            return -1;
        if (a.Time > b.Time)
            return 1;
        return 0;
    }

    this.GetTimedEvents = function (position) {

        var currentPosition = parseInt(position.toFixed(0));
        var startPosition = currentPosition;
        var endPosition = startPosition + 250;  // look ahead 250ms
        var currentEventsFired = {};
        var eventsToFire = [];
        var hasCurrentEvents = false;
        if (timedEvents.length == 0) return eventsToFire;
        var index = 0;

        if (startPosition > 0 && startPosition < 1000 && isFirstUse) {
            //Get any events between 0 and first registered time
            getEventsForTime(index, 0, startPosition);
            isFirstUse = false;
        }

        index = self.TimingSearcher.GetStartIndex(startPosition);
        getEventsForTime(index, startPosition, endPosition);

        if (hasCurrentEvents) {
            lastEventsFired = {};
            lastEventTime = currentPosition;
            for (var key in currentEventsFired) {
                lastEventsFired[key] = currentEventsFired[key];
            }
        }
        else {
            //clear last events if none fired for 1000ms
            if (lastEventTime + 1000 < currentPosition) {
                lastEventsFired = {};
            }
        }

        return eventsToFire;

        function getEventsForTime(startIndex, startTime, endTime) {

            for (startIndex; startIndex < timedEvents.length; startIndex++) {
                var timedEvent = timedEvents[startIndex];

                if (timedEvent.Time >= startTime && timedEvent.Time < endTime && lastEventsFired[timedEvent.Id] == undefined) {
                    eventsToFire.push(timedEvent);
                    currentEventsFired[timedEvent.Id] = timedEvent;
                    hasCurrentEvents = true;
                }

                if (timedEvent.Time >= endTime) {
                    break; // jump out
                }
            }
        }

    };


};


Mediasite.Player.TimedEventUtility = {};
Mediasite.Player.TimedEventUtility.toProperTimedEvent = (function () {
    function resolveTimeInSeconds(asSeconds, asMilliseconds) {
        if (typeof asSeconds === 'number') {
            return asSeconds;
        } else if (typeof asMilliseconds === 'number') {
            return asMilliseconds / 1000;
        } else {
            return 0;
        }
    }

    return function (obj) {
        var result = {};
        result.id = obj.id || obj.Id;
        result.time = resolveTimeInSeconds(obj.time, obj.Time);
        result.type = obj.type || obj.Type || obj.PayloadType;
        result.payload = obj.payload || obj.Payload || obj.Text;
        return result;
    }
}());



Mediasite.Player.LookupTable = function () {
    var self = this;
    var things = {};
    var keyPrefix = 'key--';

    // Sanitizes a key for use as a property name/key. 
    // For example, a key of 'hasOwnProperty' becomes 'key--hasOwnProperty'.
    function idd(key) {
        return keyPrefix + key;
    }

    this.add = function (key, thing) {
        // TODO: Validate that key is a string.
        things[idd(key)] = thing;
    };

    this.remove = function (key) {
        // TODO: Validate that key is a string.
        delete things[idd(key)];
    };

    this.getAll = function () {
        var array = [];
        var prop;

        for (prop in things) {
            if (things.hasOwnProperty(prop)) {
                array.push(things[prop]);
            }
        }

        return array;
    };

    this.getByKey = function (key) {
        // TODO: Validate that key is a string.
        return things[idd(key)];
    };

    this.hasItemForKey = function (key) {
        return !! self.getByKey(key);
    };
}


Mediasite.Player.EventBundle = function (expectedEvents) {

    var self = this;
    var _eventList = {};
    var _expectedEventNameExpression = /^[\w\$]+$/;

    if ($.isArray(expectedEvents)) {
        addSpecializedFireMethods(expectedEvents);
    }

    this.addHandler = function (eventName, handler) {
        if (typeof eventName == 'object') {
            actOnEventDictionary(eventName, addSingleHandler);
        } else {
            addSingleHandler(eventName, handler);
        }
    };

    this.removeHandler = function (eventName, handler) {
        if (typeof eventName == 'object') {
            actOnEventDictionary(eventName, removeSingleHandler);
        } else {
            removeSingleHandler(eventName, handler);
        }
    };

    this.removeAllHandlers = function () {
        _eventList = {};
    };

    this.fire = function (eventName /*, handlerArgs */) {
        if (_eventList[eventName]) {
            var handlerArgs = Array.prototype.slice.call(arguments, 1);
            _eventList[eventName].fire.apply(_eventList[eventName], handlerArgs);
        }
    };


    function addSpecializedFireMethods(events) {
        for (var i = 0, count = events.length; i < count; i++) {
            var event = events[i];
            if (typeof event !== 'string' || !isFunctionNameCompatible(event)) {
                throw { name: 'InvalidEventName' };
            }
            addSpecializedFireMethod(event);
        }
    }

    function addSpecializedFireMethod(eventName) {
        self['fire_' + eventName] = function () {
            var args = Array.prototype.slice.apply(arguments);
            args.unshift(eventName);
            self.fire.apply(self, args);
        };
    }

    function isFunctionNameCompatible(string) {
        return _expectedEventNameExpression.test(string);
    }

    function actOnEventDictionary(dictionary, action) {
        for (name in dictionary) {
            if (dictionary.hasOwnProperty(name)) {
                var value = dictionary[name];
                if (typeof value == 'function') {
                    action(name, value);
                }
            }
        }
    }

    function addSingleHandler(eventName, handler) {
        // /* DEBUG ONLY!!! */ if (typeof eventName != "string" || !eventName.length) window.console && console.error("Could not assign event handler to undefined event name");

        if (!_eventList[eventName]) {
            _eventList[eventName] = $.Callbacks();
        }
        _eventList[eventName].add(handler);
    }

    function removeSingleHandler(eventName, handler) {
        if (_eventList[eventName]) {
            _eventList[eventName].remove(handler);
        }
    }

};


Mediasite.namespace("Player.FeatureAccess");
Mediasite.Player.FeatureAccess = function () {
    var _disabled = {};

    this.disable = function (key, disable) {
        // /* DEBUG ONLY!!! */ if (typeof key == "undefined") window.console && console.error("Could not disable - missing key");
        _disabled[key] = (arguments.length == 1 || !!disable)
    };

    this.isDisabled = function (key) {
        // /* DEBUG ONLY!!! */ if (typeof key == "undefined") window.console && console.error("Could not check if disabled - missing key");
        // Keys are enabled by default
        return _disabled[key] === true;
    }
};

Mediasite.Player.PlayerStateManagerEvent = {
    PlayerStateChanged: 'PlayerStateChanged'
};

Mediasite.Player.ViewEvent = {
    PreferenceChanged: 'PreferenceChanged',
    LayoutLoaded: 'LayoutLoaded',
    LayoutChanged: 'LayoutChanged',
    FullScreen: 'FullScreen',
    PopOut: 'PopOut',
    PlayAsVideo: 'PlayAsVideo',
    VisibleStreamsChanged: 'VisibleStreamsChanged',
    
    TogglePlayPause: 'TogglePlayPause',
    PositionChanged: 'PositionChanged',
    SkipBack: 'SkipBack',
    SkipForward: 'SkipForward',
    
    PlaybackRateChanged: 'PlaybackRateChanged',
    PlaybackRateFaster: 'PlaybackRateFaster',
    PlaybackRateSlower: 'PlaybackRateSlower',
    
    SlideChanged: 'SlideChanged',
    PreviousSlide: 'PreviousSlide',
    NextSlide: 'NextSlide',
    FirstSlide: 'FirstSlide',
    LastSlide: 'LastSlide',
    
    ToggleSlideShow: 'ToggleSlideShow',
    ShowSlideList: 'ShowSlideList',
    ShowSlideSmallThumbs: 'ShowSlideSmallThumbs',
    ShowSlideLargeThumbs: 'ShowSlideLargeThumbs',
    ShowSlideText: 'SlideShowText',

    ShowChapters: 'ShowChapters', 
    ChapterChanged: 'ChapterChanged',
    
    AskQuestion: 'AskQuestion',
    SendQuestion: 'SendQuestion',
    
    OpenEmail: 'OpenEmail',
    SendEmail: 'SendEmail',
    
    ShowHelp: 'ShowHelp',
    ShowInfo: 'ShowInfo',

    ShowLinks: 'ShowLinks',
    ShowPolls: 'ShowPolls',
    
    ToggleCaptions: 'ToggleCaptions',
    
    ToggleSearch: 'ToggleSearch',
    Search: 'Search',

    DismissPanels: 'DismissPanels',
    ToggleScreenReader: 'ToggleScreenReader',
    ToggleLogViewer: 'ToggleLogViewer',    
    
    VolumeUp: 'VolumeUp',
    VolumeDown: 'VolumeDown',
    VolumeChanged: 'VolumeChanged',
    AudioLanguageChanged: 'AudioLanguageChanged',
    Mute: 'Mute',    
    UpdateAskQuestionEmailAddress: 'UpdateAskQuestionEmailAddress',

    NextView: 'NextView',
    PreviousView: 'PreviousView',

    ToggleStageLightbox1: 'ToggleStageLightbox1',
    ToggleStageLightbox2: 'ToggleStageLightbox2',
    ToggleStageLightbox3: 'ToggleStageLightbox3',
    ToggleStageLightbox4: 'ToggleStageLightbox4',

    JumpForward: 'JumpForward',
    JumpForwardMore: 'JumpForwardMore',
    JumpForwardStatic: 'JumpForwardStatic',
    JumpBack: 'JumpBack',
    JumpBackMore: 'JumpBackMore',
    JumpBackStatic: 'JumpBackStatic',
    ShortcutsList: 'ShortcutsList'
};

Mediasite.Player.ViewEvent.fromKeyEvent = function (keyEvent) {
    if (keyEvent.keyCode != 27 && (keyEvent.target.tagName == 'INPUT' || keyEvent.target.tagName == 'TEXTAREA')) return;

    switch (keyEvent.keyCode) {
        case 27: // escape
            return Mediasite.Player.ViewEvent.DismissPanels;
            break;
        case 32:  //space
            if (keyEvent.target.tagName == "BUTTON") {
                //follow native browser spacebar behavior
                return;
            }

            return Mediasite.Player.ViewEvent.TogglePlayPause;
            break;
        case 37: // left arrow
            if (keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.PreviousSlide;
            }
            if (keyEvent.ctrlKey && keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.PreviousSlide;
            }
            if (!keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.SkipBack;
            } 
            break;
        case 38:  // up arrow
            if (keyEvent.ctrlKey || keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.VolumeUp;
            }
            else {
                return Mediasite.Player.ViewEvent.PlaybackRateFaster;
            }
            break;
        case 39:  // right arrow
            if (keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.NextSlide;
            }
            if (keyEvent.ctrlKey && keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.NextSlide;
            }
            if (!keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.SkipForward;
            }
            break;
        case 40:  // down arrow
            if (keyEvent.ctrlKey || keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.VolumeDown;
            }
            else {
                return Mediasite.Player.ViewEvent.PlaybackRateSlower;
            }
            break;
        case 65:  //A ask
            return Mediasite.Player.ViewEvent.AskQuestion;
            break;
        case 67:  //C chapters + captions
            if (keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.ToggleCaptions;
            }
            else {
                return Mediasite.Player.ViewEvent.ShowChapters;
            }
            break;
        case 69:  //E email              
            return Mediasite.Player.ViewEvent.OpenEmail;
            break;
        case 70:  //F search (Find)                        
            return Mediasite.Player.ViewEvent.ToggleSearch;
            break;
        case 72: // H help
        case 112: //F1 help 
            return Mediasite.Player.ViewEvent.ShowHelp;
            break;
        case 73: // I info panel
            return Mediasite.Player.ViewEvent.ShowInfo;
            break;
        case 76:  //L links && logging
            if (keyEvent.ctrlKey && keyEvent.shiftKey && keyEvent.altKey) {
                return Mediasite.Player.ViewEvent.ToggleLogViewer;
            } else {
                return Mediasite.Player.ViewEvent.ShowLinks;
            }
            break;
        case 77:  //M mute
            return Mediasite.Player.ViewEvent.Mute;
            break;
        case 80:  //P polls                         
            return Mediasite.Player.ViewEvent.ShowPolls;
            break;
        case 81:  //Q polls
            if (keyEvent.ctrlKey) {
                return Mediasite.Player.ViewEvent.ShortcutsList;
            }
            break;
        case 83:  //S slide + slide text
            if (keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.ShowSlideText;
            }
            else {
                return Mediasite.Player.ViewEvent.ToggleSlideShow;
            }
            break;
        case 90: // Z
            if (keyEvent.ctrlKey) {
                return Mediasite.Player.ViewEvent.ToggleScreenReader;
            }
            break;
        case 188:  // comma or  <
            return Mediasite.Player.ViewEvent.PreviousView;
            break;
        case 190:  // period or  >
            return Mediasite.Player.ViewEvent.NextView;
            break;
        case 189: // - sign
            return Mediasite.Player.ViewEvent.VolumeDown;
            break;
        case 187: // + sign
            return Mediasite.Player.ViewEvent.VolumeUp;
            break;
        case 219: // left bracket / brace key
            if (!keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.JumpBack;
            }
            if (!keyEvent.ctrlKey && !keyEvent.altKey && keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.JumpBackMore;
            }
            if (keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.JumpBackStatic;
            }
            break;
        case 221: // right bracket / brace key
            if (!keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.JumpForward;
            }
            if (!keyEvent.ctrlKey && !keyEvent.altKey && keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.JumpForwardMore;
            }
            if (keyEvent.ctrlKey && !keyEvent.altKey && !keyEvent.shiftKey) {
                return Mediasite.Player.ViewEvent.JumpForwardStatic;
            }
            break;
        case 49:  // 1
            return Mediasite.Player.ViewEvent.ToggleStageLightbox1;
            break;
        case 50:  // 2
            return Mediasite.Player.ViewEvent.ToggleStageLightbox2;
            break;
        case 51:  // 3
            return Mediasite.Player.ViewEvent.ToggleStageLightbox3;
            break;
        case 52:  // 4
            return Mediasite.Player.ViewEvent.ToggleStageLightbox4;
            break;
        case 97:  // 1
            return Mediasite.Player.ViewEvent.ToggleStageLightbox1;
            break;
        case 98:  // 2
            return Mediasite.Player.ViewEvent.ToggleStageLightbox2;
            break;
        case 99:  // 3
            return Mediasite.Player.ViewEvent.ToggleStageLightbox3;
            break;
        case 100:  // 4
            return Mediasite.Player.ViewEvent.ToggleStageLightbox4;
            break;
    }
};



Mediasite.namespace("Player.Navigation");
Mediasite.Player.Navigation.Units = {
    Percent: 'Percent',
    Seconds: 'Seconds'
};

Mediasite.Player.Navigation.JumpNavigationHelper = function () {

    var snapToChapterTimeDelta = 10000;
    var minimumJumpAmount = 10000;
    var jumpAmountSpec = [];

    jumpAmountSpec[Mediasite.Player.ViewEvent.JumpForward] = { Amount: 1, Units: Mediasite.Player.Navigation.Units.Percent };
    jumpAmountSpec[Mediasite.Player.ViewEvent.JumpForwardMore] = { Amount: 10, Units: Mediasite.Player.Navigation.Units.Percent };
    jumpAmountSpec[Mediasite.Player.ViewEvent.JumpForwardStatic] = { Amount: 30, Units: Mediasite.Player.Navigation.Units.Seconds };
    jumpAmountSpec[Mediasite.Player.ViewEvent.JumpBack] = { Amount: -1, Units: Mediasite.Player.Navigation.Units.Percent };
    jumpAmountSpec[Mediasite.Player.ViewEvent.JumpBackMore] = { Amount: -10, Units: Mediasite.Player.Navigation.Units.Percent };
    jumpAmountSpec[Mediasite.Player.ViewEvent.JumpBackStatic] = { Amount: -30, Units: Mediasite.Player.Navigation.Units.Seconds };

    this.isJumpViewEvent = function (viewEvent) {
        if (viewEvent == Mediasite.Player.ViewEvent.JumpForward ||
            viewEvent == Mediasite.Player.ViewEvent.JumpForwardMore ||
            viewEvent == Mediasite.Player.ViewEvent.JumpForwardStatic ||
            viewEvent == Mediasite.Player.ViewEvent.JumpBack ||
            viewEvent == Mediasite.Player.ViewEvent.JumpBackMore ||
            viewEvent == Mediasite.Player.ViewEvent.JumpBackStatic)

            return true;
        else
            return false;
    };

    // returns an object with the amount to jump and the units of the amount

    this.getjumpAmountSpecFromViewEvent = function (jumpViewEvent) {
        return jumpAmountSpec[jumpViewEvent];
    };

    // returns the amount of time to jump / navigate within the presentation.

    this.getJumpAmount = function (jumpAmountSpec, jumpViewEvent, duration) {
        if (jumpAmountSpec.Units == Mediasite.Player.Navigation.Units.Percent) {
            var jumpAmount = duration * (1 / 100) * jumpAmountSpec.Amount;

            // if this is a simple 'jump' it must not be less than the minimum jump amount

            if (jumpViewEvent == Mediasite.Player.ViewEvent.JumpForward ||
                jumpViewEvent == Mediasite.Player.ViewEvent.JumpBack) {

                if (Math.abs(jumpAmount) >= (minimumJumpAmount))
                    return jumpAmount;
                else
                    if (jumpAmount < 0)
                        return -(minimumJumpAmount);
                    else
                        return minimumJumpAmount;
            } else
                return jumpAmount;
        } else
            return jumpAmountSpec.Amount * 1000;
    };

    // returns how much time jump / navigate within the presentation, considering any chapter points to stop at within the jump interval

    this.getJumpTime = function (jumpViewEvent, currentPosition, duration, chapters) {
        var jumpAmountSpec = this.getjumpAmountSpecFromViewEvent(jumpViewEvent);
        var jumpAmount = this.getJumpAmount(jumpAmountSpec, jumpViewEvent, duration);
        var newProposedPosition = currentPosition + jumpAmount;

        // cannot go to a position beyond or before the presentation

        if (newProposedPosition > duration)
            newProposedPosition = duration;
        else if (newProposedPosition < 0)
            newProposedPosition = 0;

        var chapterToSnapTo = null;

        // consider chapter points in between the current position and where we intend to jump to.  

        if (jumpAmount < 0) {
            chapterToSnapTo = getNextChapterToSnapToGoingBackward(currentPosition, newProposedPosition, chapters);
        } else {
            chapterToSnapTo = getNextChapterToSnapToGoingForward(currentPosition, newProposedPosition, chapters);
        }

        // if we found a chapter point, return it's time to jump to.

        if (chapterToSnapTo)
            return chapterToSnapTo.Time;
        else
            return newProposedPosition;
    };

    function getNextChapterToSnapToGoingForward(currentPos, newPos, chapters) {
        var chapterToSnapTo = null;

        // return the first chapter we encounter going forward from our current position to the new position
        $.each(chapters, function (i, chapter) {

            if (chapter.Time > (newPos + snapToChapterTimeDelta))
                return false;

            // continue loop if the chapter Time is to the left of our range/tolerance
            if (chapter.Time <= currentPos)
                return true;

            chapterToSnapTo = chapter;
            return false;
        });

        return chapterToSnapTo;
    }

    function getNextChapterToSnapToGoingBackward(currentPos, newPos, chapters) {
        var chapterToSnapTo = null;
        var chaptersSortedBackward = chapters.slice(0);

        chaptersSortedBackward.sort(function (a, b) {
            if (a.Time > b.Time)
                return -1;
            else
                return 1;
        });

        // return the first chapter we encounter going backward from our current position to the new position
        $.each(chaptersSortedBackward, function (i, chapter) {

            if (chapter.Time < (newPos - snapToChapterTimeDelta))
                return false;

            // continue loop if the chapter Time is to the left of our range/tolerance
            if (chapter.Time >= currentPos)
                return true;

            chapterToSnapTo = chapter;
            return false;
        });

        return chapterToSnapTo;
    }





};

Mediasite.Player.DisableableEventBundle = function (expectedEvents) {
    var self = new Mediasite.Player.EventBundle(expectedEvents);
    $.extend(self, new Mediasite.Player.FeatureAccess());

    var fire = self.fire;
    self.fire = function (eventName) {
        if (self.isDisabled(eventName)) return;
        fire.apply(self, [].slice.call(arguments));
    };

    self.disableCommonViewEvents = function (playerLayoutOptions, presentation, mediaInfo) {
        /// Disables various view events based on common PlayerLayoutOptions, presentation options, and platform feature support.
        /// Views are responsible for disabling events based on presentation data (e.g. presentation has no chapters --> disable ChapterChanged?)
        /// per their implementation (hide vs disable button if backing data does not exist)

        var event = Mediasite.Player.ViewEvent;

        var plo = playerLayoutOptions = playerLayoutOptions || {};
        var preso = presentation = presentation || {};
        mediaInfo = mediaInfo || {};
        
        self.disable(event.TogglePlayPause, plo.AllowPlayPauseStop === false);
        self.disable(event.PositionChanged, plo.AllowScrubbing === false);
        self.disable(event.SkipBack, plo.AllowScrubbing === false || plo.AllowSkipback === false);
        self.disable(event.SkipForward, plo.AllowScrubbing === false || plo.AllowSkipForward === false);
        
        var slideNavigation = plo.AllowScrubbing === false || plo.ShowSlideNavigation === false;
        self.disable(event.SlideChanged, slideNavigation); // nix in favor of view checking ShowSlideNavigation as a layout feature?
        self.disable(event.PreviousSlide, slideNavigation);
        self.disable(event.NextSlide, slideNavigation);
        self.disable(event.LastSlide, slideNavigation);
        self.disable(event.FirstSlide, slideNavigation);

        var chapterNavigation = plo.AllowScrubbing === false || plo.AllowChapterNavigation === false;
        self.disable(event.ChapterChanged, chapterNavigation); // nix?
        self.disable(event.ShowChapters, chapterNavigation);

        var search = plo.AllowScrubbing === false || plo.EnableSearch === false;
        self.disable(event.ToggleSearch, search);
        self.disable(event.Search, search);

        var speedControl = plo.AllowSpeedControl === false || !Mediasite.Player.Support.playbackRate || mediaInfo.CanChangeSpeed === false;
        self.disable(event.PlaybackRateChanged, speedControl);
        self.disable(event.PlaybackRateFaster, speedControl);
        self.disable(event.PlaybackRateSlower, speedControl);


        self.disable(event.LayoutChanged, plo.AllowLayoutAdjustment === false);
        
        var volume = plo.AllowVolumeControl === false || !Mediasite.Player.Support.volume;
        self.disable(event.VolumeChanged, volume);
        self.disable(event.VolumeUp, volume);
        self.disable(event.VolumeDown, volume);
        
        self.disable(event.ToggleCaptions, plo.AllowClosedCaptions === false || preso.DisableCaptionDisplay === true);

        var questions = plo.AllowAskQuestion === false || preso.ForumEnabled === false;
        self.disable(event.AskQuestion, questions);
        self.disable(event.SendQuestion, questions);

        self.disable(event.ShowLinks, plo.AllowLinks === false);
        self.disable(event.ShowHelp, plo.AllowHelp === false);
        self.disable(event.ShowPolls, plo.AllowPolls === false || preso.PollingEnabled === false);
        self.disable(event.ShowInfo, plo.EnablePresentationInfo === false);

        var share = plo.AllowShare === false || preso.SharingEnabled === false;
        self.disable(event.OpenEmail, share);
        self.disable(event.SendEmail, share);
    };

    return self;
};

Mediasite.Player.ModelEvent = {
    PresentationLoaded: 'PresentationLoaded',
    InactivityExceeded: 'InactivityExceeded',
    EmailInviteLoaded: 'EmailInviteLoaded',
    UserEmailAddressLoaded: 'UserEmailAddressLoaded',
    OptionsLoaded: 'OptionsLoaded',
    StartMedia: 'StartMedia',
    AddNewSlide: 'AddNewSlide',
    CaptionChange: 'CaptionChange',
    PositionChange: 'PositionChange',
    SendQuestionComplete: 'SendQuestionComplete',
    SendEmailComplete: 'SendEmailComplete',
    SearchComplete: 'SearchComplete',
    SearchError: 'SearchError',
    LiveStatusChanged: 'LiveStatusChanged',
    SetLiveStartTime: 'SetLiveStartTime',
    RetrieveBookmark: 'RetrieveBookmark',
    MediaError: 'MediaError',
    ChapterChange: 'ChapterChange',
    TimedEventReached: 'TimedEventReached',
    TimedEventListChanged: 'TimedEventListChanged',
    TimelineMarkerAdded: 'TimelineMarkerAdded',
    TimelineMarkerRemoved: 'TimelineMarkerRemoved',
    TimelineMarkerUpdated: 'TimelineMarkerUpdated',
    PlayerStateChanged: 'PlayerStateChanged',
    VisibleStreamsChanged: 'VisibleStreamsChanged'
};

Mediasite.Player.BaseView = function () {
    return {
        SetFeatureAccess: function (featureAccessLevels) { },
        AddEventHandler: function (eventName, handler) { },
        LoadLayout: function () { },
        LoadNewOptions: function () { },
        UpdatePresentationData: function (presentation) { },
        UpdateMediaInfo: function (mediaInfo) { },
        UpdateMediaState: function (state) { },
        UpdatePlayerState: function (playerState) { },
        UpdateMediaVolume: function (volume) { },
        UpdateMediaFidelity: function (fidelityInfo) { },
        UpdatePlaybackRate: function (rateInfo) { },
        UpdatePosition: function (position) { },
        UpdateEmailInvite: function (emailData) { },
        UpdateUserEmailAddress: function (emailAddress) { },
        UpdateOptions: function (options) { },
        UpdateCurrentSlide: function (slide) { },
        UpdateCurrentCaption: function (caption) { },
        UpdateCurrentCaptions: function (captions) { },
        UpdateSlideBrowserView: function (view) { },
        UpdateVisibleStreams: function (streamIds) { },
        UpdateTimelineMarkers: function () { }, 
        AddTimelineMarker: function (timelineMarker) { },
        RemoveTimelineMarker: function (timelineMarkerId) { },
        LiveStatusChange: function (status) { },
        InactivityExceeded: function () { },
        SendQuestionComplete: function (success, message) { },
        SendEmailComplete: function (success, message) { },
        ShowErrorMessage: function (title, details) { },
        ShowChapters: function () { },
        ShowLinks: function () { },
        ShowPolls: function () { },
        ShowHelp: function () { },
        ToggleSlideText: function () { },
        ToggleSlideBrowser: function () { },
        ToggleCaptionDisplay: function () { },
        ToggleSearch: function () { },
        DisplaySearchResults: function (searchTerm, results) { },
        DisplaySearchError: function () { },
        ShowInfo: function () { },
        MediaLoadStart: function () { },
        LaunchPopOut: function (currentMediaState) { },
        GetLayoutElements: function () { },
        DismissPanels: function () { },
        GetStreamViewHandler: function (streamType) { },
        SlideStreamType: Mediasite.ContentStreamType.Slide
    };
};

Mediasite.Player.BaseModel = function () {
    return {
        Initialiize: function () { },
        LoadMedia: function () { },
        ReportMediaOpened: function (mediaPlayerType) { },
        ReportMediaView: function (mediaView, synchronous) { },
        UpdateBookmark: function (mediaView) { },
        UpdateMediaInfo: function (mediainfo) { },
        HandleMediaEvent: function (event) { },
        HandleMediaError: function (error) { },
        UpdateMediaState: function (state) { },
        UpdatePlayerState: function (playerState) { },
        GetPlayerState: function() { },
        UpdateMediaVolume: function (volume) { },
        UpdatePosition: function (position) { },
        SetCurrentSlide: function (slideNumber) { },
        SetInactivityTimeout: function (timeout) { },
        SetPreference: function (pref, val) { },
        TogglePreference: function (boolPref) { },
        NextSlide: function () { },
        PreviousSlide: function () { },
        FirstSlide: function () { },
        LastSlide: function () { },
        GetEmailInvite: function () { },
        GetUserEmailAddress: function () { },
        SendQuestion: function (question) { },
        SendEmail: function (email) { },
        EnableLiveJumpBackDetection: function () { },
        EnableLiveStartDelay: function (delayInMs) { }
    };
};

Mediasite.Player.Support = new function () {
    this.csstransforms3d = false;
    this.localstorage = false;
    this.touch = false;
    this.msTouch = false;
    this.volume = false;
    this.playbackRate = false;
    this.multipleWindows = false;

    init.apply(this);

    function init() {
        if (window.Modernizr) {
            this.csstransforms3d = Modernizr.csstransforms3d;
            this.localstorage = Modernizr.localstorage;
            this.touch = Modernizr.touchevents;
        }

        if (!(Mediasite.BrowserDetect.IOS() || Mediasite.BrowserDetect.Android())) {
            this.volume = true;
            this.playbackRate = true;
        }

        if (!(Mediasite.BrowserDetect.IOS() || Mediasite.BrowserDetect.Android() || Mediasite.BrowserDetect.Blackberry())) {
            this.multipleWindows = true;
        }

        if (window.navigator && window.navigator.msPointerEnabled) {
            this.msTouch = true;
        }
    }
};

Mediasite.Player.TimeHelper = {
    getDisplayFromDuration: function (durationInMS, padHours) {
        if (durationInMS < 0) {
            return "";
        }
        var whole = durationInMS;
        var hours = Math.floor(whole / 3600000);
        whole = whole - (hours * 3600000);
        if (hours < 10) {
            hours = "0" + hours;
        }
        var minutes = Math.floor(whole / 60000);
        whole = whole - (minutes * 60000);
        var seconds = Math.floor(whole / 1000);
        whole = whole - (seconds * 1000);
        if (minutes < 10) {
            minutes = "0" + minutes;
        }
        if (seconds < 10) {
            seconds = "0" + seconds;
        }
        if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) {
            if (!padHours) { return "00:00"; }
            return ("00:00:00");
        }
        if (hours === "00" && !padHours) {
            return (minutes + ":" + seconds);
        }
        return (hours + ":" + minutes + ":" + seconds);
    },
    getDurationFromDisplay: function (durationDisplay) {
        var i, segments = durationDisplay.split(":");

        for (i = 0; i < segments.length; i++) {
            if (!this.isNumber(segments[i]) || segments[i].length != 2 || segments[i] < 0) {
                return -1;
            }
        }

        var timeInMS = 0;

        switch (segments.length) {
            case 3:
                timeInMS = segments[0] * 3600000 + segments[1] * 60000 + segments[2] * 1000;
                break;
            case 2:
                timeInMS = segments[0] * 60000 + segments[1] * 1000;
                break;
            case 1:
                timeInMS = segments[0] * 1000;
                break;
        }

        return timeInMS;
    },
    isNumber: function (n) {
        return !isNaN(parseFloat(n)) && isFinite(n);
    }
};


Mediasite.Player.TimeHelper.Duration = function (durationInMS) {

    var whole = durationInMS;
    var hours = Math.floor(whole / (60 * 60 * 1000));
    whole = whole - (hours * (60 * 60 * 1000));
    var minutes = Math.floor(whole / (60 * 1000));
    whole = whole - (minutes * (60 * 1000));
    var seconds = Math.floor(whole / 1000);
    whole = whole - (seconds * 1000);

    function zeroPad(integer, toDigits) {
        var result = integer + '';
        while (result.length < toDigits) {
            result = '0' + result;
        }
        return result;
    }

    function digits(integer) {
        return (integer > 0) ? (integer + '').length : 0;
    }

    return {
        hours: hours,
        minutes: minutes,
        seconds: seconds,

        toString: function (options) {
            options = options || this.recommendFormattingOptions();

            var result,
                    separator = ':';

            var myMinutes = this.minutes;
            var myHours = this.hours;

            if (options.hourAsMinutes) {
                myMinutes += this.hours * 60;
                myHours = 0;
            }

            result = zeroPad(myMinutes, options.minuteDigits) + separator + zeroPad(this.seconds, 2);

            if (options.hourDigits > 0) {
                result = zeroPad(myHours, options.hourDigits) + separator + result;
            }

            return result;
        },

        recommendFormattingOptions: function () {
            var options = {
                hourAsMinutes: false,
                hourDigits: 0,
                minuteDigits: 0
            };

            var totalMinutes = this.hours * 60 + this.minutes;

            if (totalMinutes <= 60) {
                options.hourAsMinutes = true;
                options.hourDigits = 0;
                options.minuteDigits = digits(totalMinutes);
            } else {
                options.hourAsMinutes = false;
                options.hourDigits = digits(this.hours);
                options.minuteDigits = options.hourDigits > 0 ? 2 : digits(this.minutes);
            }

            // We always want to display something for minutes, even if just a zero.
            options.minuteDigits = Math.max(1, options.minuteDigits);

            return options;
        }
    };
};

Mediasite.ElementSelector = function (rootElement) {
    if (!rootElement) rootElement = document.body;
    return {
        element: function () {
            return $(rootElement);
        },
        addSelectorMethods: function (selectors) {
            for (var name in selectors) {
                if (selectors.hasOwnProperty(name)) {
                    this.addSelectionMethod(name, selectors[name]);
                }
            }
        },
        addSelectionMethod: function (name, selector) {
            this[name] = function () {
                return this.element().find(selector);
            };
        }
    };
};


Mediasite.namespace("MediaPlayer");

Mediasite.MediaPlayer.ModelEvent = {
    MediaError: 'MediaError',
    MediaLoadStart: 'MediaLoadStart',
    MediaLoaded: 'MediaLoaded',
    MediaUpdated: 'MediaUpdated',
    MediaStarted: 'MediaStarted',
    MediaEvent: 'MediaEvent',
    MediaView: 'MediaView',
    StateChanged: 'StateChanged',
    VolumeChanged: 'VolumeChanged',
    MediaFidelityChanged: 'MediaFidelityChanged',
    PlaybackRateChanged: 'PlaybackRateChanged',
    PositionChanged: 'PositionChanged',
    EnableLiveJumpBackDetection: 'EnableLiveJumpBackDetection',
    EnableLiveStartDelay: 'EnableLiveStartDelay',
    SeekStarting: 'SeekStarting',
    SeekEnding: 'SeekEnding'
};

Mediasite.MediaPlayer.BaseModel = function () {
    return {
        SetupMediaElement: function (element, sources) { },
        LiveStatusChange: function () { },
        InactivityExceeded: function () { },
        UpdateOptions: function (options) { },
        SetPosition: function (position) { },
        SetAudioLanguage: function (language) { },
        SetVolume: function (volume) { },
        SkipBack: function () { },
        SkipForward: function () { },
        PlayFaster: function () { },
        PlaySlower: function () { },
        TogglePlayPause: function () { },
        ToggleMute: function () { },
        FullScreen: function () { },
        SetLiveStartTime: function (time) { },
        RetrieveBookmark: function (bookmark) { },
        SupportsPlayAsVideo: function () { },
        GetMediaWrapper: function () { },
        FinalizeLive: function () { }
    };
};

Mediasite.MediaPlayer.BaseView = function () {
    return {
        CreateMediaElement: function () { },
        UpdateMediaInfo: function (mediaInfo) { },
        MediaLoadStart: function () { }
    };
};


Mediasite.namespace("Player.Error");
Mediasite.Player.Error.Code = {
    MethodNotFound: 410,
    MethodError: 411,
//    LoadManifestFailure: 500,
    MediaError: 510,
    MediaNotSupported: 511,
    PlayerNotReady: 520
};

Mediasite.Player.Error.Description = {
    410: "API method not found",
    411: "Error while calling API method",
    500: "Could not load presentation metadata",
    510: "Problem with presentation media",
    511: "Presentation media is not supported",
    520: "Player not ready"
};

Mediasite.Player.MediaPlugins = {
    None: 0,
    WindowsMedia: 1,
    Silverlight: 2,
    HTML5: 4,
    Flash: 8,
    Port25: 16,
    Hls: 32,
    Silverlight4: 64,
    HTML5Dash: 128,
    RampHls: 256,
    HLSjs: 512,

    All: 1023,

    Parse: function (name) {
        if (typeof (name) === 'undefined') {
            return this.None;
        }
        switch (name.toLowerCase()) {
            case "windowsmedia":
                return this.WindowsMedia;
            case "port25":
                return this.Port25;
            case "html5":
            case "hls":
                return this.HTML5;
            case "hlsjs":
                return this.HLSjs;
            case "silverlight":
                return this.Silverlight;
            case "html5dash":
                return this.HTML5Dash;
            case "flash":
                return this.Flash;
            case "ramphls":
                return this.RampHls;
            default:
                return this.None;
        }
    },

    GetDisplayNames: function (plugins) {
        if (!plugins) {
            return ["None"];
        }

        var names = [];
        if (plugins & this.WindowsMedia) names.push("WindowsMedia");
        if (plugins & this.Silverlight) names.push("Silverlight");
        if (plugins & this.Flash) names.push("Flash");
        if (plugins & this.HTML5) names.push("HTML5");
        if (plugins & this.Port25) names.push("Port25");
        if (plugins & this.Hls) names.push("Hls");
        if (plugins & this.HLSjs) names.push("HLS.js");
        if (plugins & this.HTML5Dash) names.push("HTML5Dash");
        if (plugins & this.RampHls) names.push("RampHls");

        return names;
    },

};

Mediasite.Player.MimeTypes =
{
    WMV: 'video/x-ms-wmv',
    MP4: 'video/mp4',
    SS: 'video/x-mp4-fragmented',
    M3U: 'audio/x-mpegurl',
    Dash: 'video/x-mpeg-dash'
};

Mediasite.Player.ServerTypes =
{
    IIS: 'IIS',
    WMS: 'WMS',
    Ramp: 'Ramp',
    Deliver: 'Deliver'
};

Mediasite.Player.MediaTypes =
{
    MP4: 'MP4',
    WMV: 'WMV',
    SS: 'SS',
    Dash: 'Dash'
};

Mediasite.Player.PlaybackOptions = function (presentation, serverBookmark) {

    this.StartPosition = 0;
    this.AutoStart = true;
    this.PauseAt = -1;
    var self = this;

    function getStartPosition() {

        var startPosition = 0;
        try {
            startPosition = parseInt(Mediasite.GetQueryParam("playFrom"), 10);
            if (isNaN(startPosition) && !isLive(presentation)) {
                //Determine bookmark position
                var bookmarkStore = new Mediasite.Player.BookmarkStore(presentation.PresentationId, presentation.Duration, false, presentation.ServerClockSkew, self);
                var clientBookmark = bookmarkStore.retrieve();

                if (!serverBookmark) {
                    serverBookmark = { savedAt: 0, position: 0 };
                }

                // The client bookmark has greater precision, so if the client 
                // and server bookmark positions are close enough, use the 
                // client value.
                // 
                // If they are not really close, use the one saved later.

                if (Math.abs(serverBookmark.position - clientBookmark.position) < 1000) {
                    startPosition = clientBookmark.position;
                } else {
                    startPosition = (serverBookmark.savedAt > clientBookmark.savedAt) ? serverBookmark.position : clientBookmark.position;
                }
            }
        } catch (ex) {
        }

        return startPosition;
    }


    function getAutoStart() {

        // Set forcedAutoStart from autoStart param
        var autoStart = undefined;
        var autoStartParam = Mediasite.GetQueryParam("autoStart").toLowerCase();

        if (autoStartParam === "true") {
            autoStart = true;
        } else if (autoStartParam === "false") {
            autoStart = false;
        }

        if (autoStart == undefined) {
            autoStart = !Mediasite.BrowserDetect.Embedded();
        }

        // Mobile never attempts to auto-start.
        if (Mediasite.BrowserDetect.Mobile()) {
            autoStart = false;
        }

        return autoStart;
    }

    function getPauseAt() {

        var pauseAt = -1;

        try {
            var durationParam = parseInt(Mediasite.GetQueryParam("duration"), 10);
            if (!isNaN(durationParam)) {
                pauseAt = self.StartPosition + durationParam;
            }
        } catch (ex) {
        }

        return pauseAt;
    }

    function isLive() {

        if (presentation.PlayStatus) {
            return presentation.PlayStatus.indexOf('Live') >= 0;
        }

        return false;
    }

    function loadOptions() {
        self.StartPosition = getStartPosition();
        self.AutoStart = getAutoStart();
        self.PauseAt = getPauseAt();
    }

    $(function () {
        loadOptions();
    });
};

Mediasite.Player.UserPreferences = function () {

    var cookie = new Mediasite.Cookie("MediasitePlayerOptions");

    this.GetValue = function (key, defaultValue) {
        return cookie.GetValue(key, defaultValue);
    };

    this.GetBoolValue = function (key, defaultValue) {
        return cookie.GetBoolValue(key, defaultValue);
    };

    this.SetValue = function (key, value) {
        cookie.SetValue(key, value);
    };

    this.ToggleValue = function (key) {
        var value = cookie.GetBoolValue(key, false);
        this.SetValue(key, !value);
    };

    this.GetAll = function () {
        return cookie.GetAll();
    };


}

Mediasite.Player.SlideImage = function(slideNumber, slideUrlProvider) {
    var _slideUrlProvider = slideUrlProvider;
    var _slideNumber = slideNumber;

    this.getSlideNumber = function() { return _slideNumber; };

    this.getFullUrl = function() {
        return _slideUrlProvider.GetSlideUrl(_slideNumber);
    };

    this.getCacheFriendlyUrl = function(height, aspect) {
        return _slideUrlProvider.GetCacheFriendlySlideUrl(_slideNumber, height, aspect);
    };
};

// ===================================================================
// A basic implementation of the Mediasite.Player.SlideImage interface
// ===================================================================
Mediasite.Player.SpecialSlideImage = function (imageFilename) {
    return {
        getSlideNumber: function () { return 0; },
        getFullUrl: function () { return imageFilename; },
        getCacheFriendlyUrl: function (height, aspect) { return imageFilename; }
    };
};

// NOTE - Candidate for general string utility or String prototype extensions
Mediasite.Player.ImageUrlPlayticketFormatter = function (imageUrl, playticketId) {
    if (playticketId && playticketId.length > 0) {
        if (imageUrl.indexOf('?') > -1) {
            imageUrl += '&playbackTicket=' + playticketId;
        } else {
            imageUrl += '?playbackTicket=' + playticketId;
        }
    }
    return imageUrl;
};

Mediasite.Player.doAllPluginDetection = function (playerContext) {
    var result = {};
    var rampReceiverUrl = Mediasite.Player.getFirstRampRecieverAddress(playerContext.CurrentPresentation);

    if (playerContext.GlobalOptions.IsLayoutPreview) {
        result.desired = Mediasite.Player.MediaPlugins.None;
        result.supported = Mediasite.Player.MediaPlugins.All;
        result.available = Mediasite.Player.MediaPlugins.All;
        result.mediaCompatible = Mediasite.Player.MediaPlugins.All;
    } else {
        result.desired = Mediasite.Player.getDesiredPlugins();
        result.supported = Mediasite.Player.detectClientCapabilities(rampReceiverUrl, !playerContext.GlobalOptions.DisableChromeFlashAssumption);
        result.available = (result.desired == Mediasite.Player.MediaPlugins.None) ? result.supported : result.supported & result.desired;
        result.mediaCompatible = Mediasite.Player.getPluginsForPresentation(playerContext.CurrentPresentation, playerContext.GlobalOptions.IsLayoutPreview, playerContext.GlobalOptions.FavorDeliverHLS);
    }

    return result;
};

Mediasite.Player.showZeroPlaybackStrategiesError = function (plugins, useScreenReader) { // plugins is an object with properties: desired, supported, mediaCompatible, and available.

    var pluginRequireMeta = $('#PluginRequiredMeta');
    if (Mediasite.BrowserDetect.IEMetro() && pluginRequireMeta.length == 0) {
        // Reload page (so we can set requireActiveX header for IE10)
        // Set cookie for player capabilities error
        var cookie = new Mediasite.Cookie(Mediasite.Player.Options.PlayerCapabilitiesError);
        cookie.SetValue(Mediasite.Player.Options.PlayerCapabilitiesError, true);
        window.location.reload();
    } else {
        showMediaPlayerTypeLoadingError();
    }




    function getErrorMessage() {

        var infoText = '';
        var caps = {};
        caps.SupportedPlugins = plugins.supported;
        caps.CompatiblePlugins = plugins.mediaCompatible;
        caps.DesiredPlugins = plugins.desired;
        caps.UseScreenReader = useScreenReader;

        var errorMessage = Mediasite.Player.Localization.ClientDetection.ErrorPageClientLoadError;

        if (errorMessage.length > 0) {
            infoText += errorMessage + "<br/>";
        }

        infoText += '</br>';
        infoText += Mediasite.Player.Localization.ClientDetection.CompatiblePlugins;
        infoText += '</br><span class="boldText">';
        infoText += getPluginString(caps.CompatiblePlugins);
        infoText += '</span></br>';



        infoText += '</br>';
        infoText += Mediasite.Player.Localization.ClientDetection.DetectedPlugins;
        infoText += '</br><span class="boldText">';
        infoText += getPluginString(caps.SupportedPlugins);
        infoText += '</span></br>';

        if (caps.DesiredPlugins != Mediasite.Player.MediaPlugins.None) {
            infoText += '</br>';
            infoText += Mediasite.Player.Localization.ClientDetection.RequestedPlugins;
            infoText += '</br><span class="boldText">';
            infoText += getPluginString(caps.DesiredPlugins);
            infoText += '</span></br>';
        }

        if (caps.UseScreenReader) {
            infoText += '</br>';
            infoText += Mediasite.Player.Localization.ClientDetection.RequestedCapabilities;
            infoText += '</br><span class="boldText">';
            infoText += Mediasite.Player.Localization.ClientDetection.ScreenReader;
            infoText += '</span></br>';
        }

        var ieVersion = Mediasite.BrowserDetect.InternetExplorer();
        if (ieVersion && ieVersion > 9) {
            infoText += '</br><span class="boldText">';
            infoText += Mediasite.Player.Localization.ClientDetection.Windows8;
            infoText += '</span></br>';
        }

        // Maybe add the Install Flash button.
        var flashCanPlayMedia = hasFlag(caps.CompatiblePlugins, Mediasite.Player.MediaPlugins.Flash);
        var hasFlash = hasFlag(caps.SupportedPlugins, Mediasite.Player.MediaPlugins.Flash);
        var askedForOtherPlugin = (caps.DesiredPlugins != Mediasite.Player.MediaPlugins.None) && !hasFlag(caps.DesiredPlugins, Mediasite.Player.MediaPlugins.Flash);
        if (isFlashPlaybackPossibleForBrowser() && flashCanPlayMedia && !hasFlash && !askedForOtherPlugin) {
            infoText += getEnableFlashMarkup();
        }

        return infoText;
    }

    function showSilverlightPromptForIE8(loadingDiv) {
        var requiredSilverlightVersion = '5.0.61118.0';
        var upgradeSilverlightMessage;
        var ieVersion = Mediasite.BrowserDetect.InternetExplorer();

        if (!ieVersion || ieVersion > 8) {
            return;
        }

        var someOtherPluginDesired = plugins.desired != Mediasite.Player.MediaPlugins.None && !hasFlag(plugins.desired, Mediasite.Player.MediaPlugins.Silverlight);
        var upgradeSilverlight = hasFlag(plugins.supported, Mediasite.Player.MediaPlugins.Silverlight4) && !hasFlag(plugins.supported, Mediasite.Player.MediaPlugins.Silverlight);

        if (hasFlag(plugins.mediaCompatible, Mediasite.Player.MediaPlugins.Silverlight)
            && !hasFlag(plugins.supported, Mediasite.Player.MediaPlugins.Silverlight)
            && !someOtherPluginDesired) {
            // Need to install/upgrade Silverlight

            var silverlightContainer = $('<div id="SilverlightContainer"></div>');
            silverlightContainer.appendTo(loadingDiv);

            Silverlight.createObjectEx({
                properties: { version: requiredSilverlightVersion },
                parentElement: silverlightContainer.get(0),
                id: silverlightContainer.id,
                context: null
            });

            if (upgradeSilverlight) {
                upgradeSilverlightMessage = $("<p>" + Mediasite.Player.Localization.ClientDetection.UpgradeSilverlight + "</p>");
                upgradeSilverlightMessage.appendTo(loadingDiv);
            }
        }
    }

    function getEnableFlashMarkup() {

        var titleText = Mediasite.Player.Localization.ClientDetection.DownloadFlashText;
        var enableMessage = '<a href="http://www.adobe.com/go/getflashplayer" title="' + titleText + '" target="_blank" border="0"><img src="Common/css/images/160x41_Get_Flash_Player.jpg" /></a>';

        if (Mediasite.BrowserDetect.Chrome()) {
            enableMessage = Mediasite.Player.Localization.ClientDetection.EnableFlashInChrome;
        } else if (Mediasite.BrowserDetect.Edge()) {
            enableMessage = Mediasite.Player.Localization.ClientDetection.EnableFlashInEdge;
        }

        return '<div class="enableFlash">' + enableMessage + '</div>';

        // If we ever need to offer an Install Flash button, do something like the following.
        // return '<p><br /><br /><a href="http://www.adobe.com/go/getflashplayer" target="_blank" border="0"><img src="Common/css/images/160x41_Get_Flash_Player.jpg" /></a></p>';
    }

    function canShowSilverlight() {
        var showSilverlight = true;


        if (hasFlag(plugins.supported, Mediasite.Player.MediaPlugins.Silverlight)) {
            showSilverlight = false;
        }
        else {
            // Mask Silverlight for iOS/Android devices
            if (Mediasite.BrowserDetect.Android() || Mediasite.BrowserDetect.IOS()) {
                showSilverlight = false;
            }

            // Mask Silverlight for Chrome browsers. (Silverlight no longer works in Chrome.)
            if (Mediasite.BrowserDetect.Chrome()) {
                showSilverlight = false;
            }

            //Mask Silverlight for IE on ARM processor (Windows 8 RT)
            if (Mediasite.BrowserDetect.InternetExplorerARM()) {
                showSilverlight = false;
            }

            //Mask Silverlight for Edge Browser
            if (Mediasite.BrowserDetect.Edge()) {
                showSilverlight = false;
            }

            // Mask Silverlight error because the user specifically asked for something besides Silverlight
            if (plugins.desired != Mediasite.Player.MediaPlugins.None && !hasFlag(plugins.desired, Mediasite.Player.MediaPlugins.Silverlight)) {
                showSilverlight = false;
            }
        }

        return showSilverlight;
    }

    function isFlashPlaybackPossibleForBrowser() {
        return !Mediasite.BrowserDetect.Mobile();
    }

    function hasFlag(flags, testFlag) {
        return (flags & testFlag) == testFlag;
    }

    function getPluginString(plugins) {

        var pluginStrings = [];
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.WindowsMedia)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.WindowsMedia); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.Silverlight)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.Silverlight); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.Silverlight4)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.Silverlight4); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.Flash)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.Flash); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.HTML5)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.Html5); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.HTML5Dash)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.Html5Dash); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.HLSjs)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.HLSjs); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.Port25)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.Port25); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.Hls)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.Hls); }
        if (hasFlag(plugins, Mediasite.Player.MediaPlugins.RampHls)) { pluginStrings.push(Mediasite.Player.Localization.ClientDetection.RampHls); }

        var pluginString = pluginStrings.join(', ');

        if (!pluginString.length) {
            pluginString = Mediasite.Player.Localization.ClientDetection.NoPlugins;
        }

        return pluginString;

    }

    function showMediaPlayerTypeLoadingError() {
        $('body').empty();

        var loadingDiv = $('<div id="loadingDiv"></div>');
        var clientDetectMessage = $('<div id="clientDetectMessage"></div>');

        clientDetectMessage.appendTo(loadingDiv);

        clientDetectMessage.html(getErrorMessage());

        showSilverlightPromptForIE8(loadingDiv);

        $('body').css({ textAlign: 'center' });
        loadingDiv.css({ fontSize: '16px', fontWeight: 'normal', width: '650px', marginTop: '20px', padding: '20px', border: '2px solid #696969', color: '#333333', backgroundColor: '#E8E8E8', borderRadius: '10px', marginLeft: 'auto', marginRight: 'auto' });
        loadingDiv.appendTo('body');
    }
}

Mediasite.Player.detectClientCapabilities = function (rampReceiverUrl, assumeChromeFlashSupport) {
    var mediaPlugins = Mediasite.Player.MediaPlugins;
    var requiredSilverlightVersion = '5.0.61118.0';
    var rampReceiverReached = undefined;
    var clientCaps = mediaPlugins.None;

    function detectSL() {
        if (Mediasite.BrowserDetect.Chrome()) {
            return;
        }

        try {
            if (window.Silverlight && Silverlight.isInstalled(requiredSilverlightVersion)) {
                clientCaps = clientCaps | mediaPlugins.Silverlight;
            }
        }
        catch (e) {
        }
    }

    function detectSL4() {
        if (Mediasite.BrowserDetect.Chrome()) {
            return;
        }

        try {
            if (window.Silverlight && Silverlight.isInstalled("4")) {
                clientCaps = clientCaps | mediaPlugins.Silverlight4;
            }
        }
        catch (e) {
        }
    }

    function detectFlash() {
        if (assumeChromeFlashSupport && Mediasite.BrowserDetect.Chrome() && !Mediasite.BrowserDetect.Mobile()) {
            clientCaps = clientCaps | mediaPlugins.Flash;
            return;
        }

        try {
            var fpv = swfobject.getFlashPlayerVersion();

            if (Mediasite.Player.MinimumFlashPlayerValues.convertToVersion(fpv.major, fpv.minor) >= Mediasite.Player.MinimumFlashPlayerValues.getMinimumVersion()) {
                clientCaps = clientCaps | mediaPlugins.Flash;
            }
        }
        catch (e) {
        }
    }

    function detectWMP() {
        var i;
        try {
            if (window.ActiveXObject !== undefined) {
                try {
                    var control = new ActiveXObject('WMPlayer.OCX.7');
                    clientCaps = clientCaps | mediaPlugins.WindowsMedia;
                    control = null;
                    return;
                }
                catch (e1) {
                }
            }
            if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
                for (i = 0; i < navigator.plugins.length; i = i + 1) {
                    if (navigator.plugins[i].filename === 'np-mswmp.dll') {
                        clientCaps = clientCaps | mediaPlugins.WindowsMedia | mediaPlugins.Port25;
                        return;
                    }
                }
            }
        }
        catch (e2) {
        }
    }

    function detectHTML5() {
        try {
            var video = document.createElement('video');
            var playable = video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');
            if (playable === 'probably' || playable === 'maybe') {
                clientCaps = clientCaps | mediaPlugins.HTML5;
            }
        }
        catch (e1) {
        }
    }

    function detectHTML5Dash() {
        try {
            if (!!window.MediaSource) {
                var video = document.createElement('video');
                var playable = video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');
                if (playable === 'probably' || playable === 'maybe') {
                    clientCaps = clientCaps | mediaPlugins.HTML5Dash;
                }
            }
        } catch (e1) {
        }
    }

    function detectHLS() {
        if (canPlayHls()) {
            clientCaps = clientCaps | mediaPlugins.Hls;
        }
    }

    function detectHLSjs() {
        if (Mediasite.BrowserDetect.MacSafari()) {
            // Mac Safari MSE-based HLS playback breaks because the current Mediasite HLS 
            // content uses different track numbers for each bitrate.
            // Exclude Mac Safari from MSE playback until it's known to work.
            return;
        }

        if (window.Hls && window.Hls.isSupported()) {
            clientCaps = clientCaps | mediaPlugins.HLSjs;
        }
    }

    function canPlayHls() {
        try {
            var video = document.createElement('video');

            var m3u8Result = video.canPlayType('application/x-mpegurl');
            var isM3u8Playable = (m3u8Result === 'probably' || m3u8Result === 'maybe');

            var h264Result = video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');
            var isH264Playable = (h264Result === 'probably' || h264Result === 'maybe');

            return isM3u8Playable && isH264Playable;
        }
        catch (e1) {
            return false;
        }

    }

    function detectRampHls() {
        if (Mediasite.BrowserDetect.MacSafari()) {
            // Mac Safari MSE-based HLS playback breaks because the current Mediasite HLS 
            // content uses different track numbers for each bitrate.
            // Exclude Mac Safari from MSE playback until it's known to work.
            return;
        }

        if (!window.Hls || !window.Hls.isSupported()) {
            return;
        }

        if (canReachRampReceiver()) {
            clientCaps = clientCaps | mediaPlugins.RampHls;
        }
    }

    function canReachRampReceiver() {
        var maxAttempts = 3;
        var attempts;

        if (typeof rampReceiverReached == 'boolean') {
            return rampReceiverReached;
        }

        if (typeof rampReceiverUrl !== 'string') {
            return false;
        }

        for (attempts = 0; attempts < maxAttempts; attempts += 1) {
            $.ajax({
                async: false,
                timeout: 1 * 1000,
                type: "GET",
                dataType: "json",
                url: rampReceiverUrl + 'rme/status?maddr=127.0.0.1:32000&_=' + (new Date()).getTime(),
                success: function () {
                    rampReceiverReached = true;
                },
                error: function () {
                    rampReceiverReached = false;
                }
            });

            if (rampReceiverReached) {
                break;
            }
        }

        return rampReceiverReached;
    }


    detectSL();
    detectSL4();
    detectFlash();
    detectWMP();
    detectHTML5();
    detectHTML5Dash();
    detectHLS();
    detectHLSjs();
    detectRampHls();

    return clientCaps;
};

Mediasite.Player.getDesiredPlugins = function () {

    var desiredPlugins = Mediasite.Player.MediaPlugins.None;

    var useWMP = Mediasite.GetQueryParam("useWMP", Mediasite.GetQueryParam.AsBool);
    var useSilverlight = Mediasite.GetQueryParam("useSilverlight", Mediasite.GetQueryParam.AsBool);
    var useFlash = Mediasite.GetQueryParam("useFlash", Mediasite.GetQueryParam.AsBool);
    var useHtml5 = Mediasite.GetQueryParam("useHTML5", Mediasite.GetQueryParam.AsBool);
    var useHLSjs = Mediasite.GetQueryParam("useHLSjs", Mediasite.GetQueryParam.AsBool);
    var useHtml5Dash = Mediasite.GetQueryParam("useHtml5Dash", Mediasite.GetQueryParam.AsBool);
    var useRampHls = Mediasite.GetQueryParam("useRampHls", Mediasite.GetQueryParam.AsBool);

    if (useWMP === true) {
        desiredPlugins = desiredPlugins | Mediasite.Player.MediaPlugins.WindowsMedia | Mediasite.Player.MediaPlugins.Port25;
    }

    if (useSilverlight === true) {
        desiredPlugins = desiredPlugins | Mediasite.Player.MediaPlugins.Silverlight;
    }

    if (useFlash === true) {
        desiredPlugins = desiredPlugins | Mediasite.Player.MediaPlugins.Flash;
    }

    if (useHtml5 === true) {
        desiredPlugins = desiredPlugins | Mediasite.Player.MediaPlugins.HTML5 | Mediasite.Player.MediaPlugins.Hls;
    }

    if (useHtml5Dash === true) {
        desiredPlugins = desiredPlugins | Mediasite.Player.MediaPlugins.HTML5Dash;
    }

    if (useRampHls === true) {
        desiredPlugins = desiredPlugins | Mediasite.Player.MediaPlugins.RampHls;
    }

    if (useHLSjs === true) {
        desiredPlugins = desiredPlugins | Mediasite.Player.MediaPlugins.HLSjs;
    }

    return desiredPlugins;
}

Mediasite.Player.getPluginsForPresentation = function (presentation, isLayoutPreview, liveFavorHls) {

    if (isLayoutPreview) { // playerContext.GlobalOptions.IsLayoutPreview
        return Mediasite.Player.MediaPlugins.HTML5;
    }

    function getVideoUrls() {

        var videoUrls = [];
        if (typeof presentation !== "undefined" && typeof presentation.Streams !== "undefined") {
            $.each(presentation.Streams, function (index, stream) {
                if (stream.VideoUrls.length > 0 && videoUrls.length == 0) {
                    videoUrls = stream.VideoUrls;
                }
            });
        }

        return videoUrls;
    }

    var isLive = presentation.PlayStatus.indexOf('Live') >= 0;
    var calc = Mediasite.MediaPlayer.MediaPlayerSelectionEngine.create(isLive, liveFavorHls);
    var calculated = calc.calculateMediaPlayers(Mediasite.Player.MediaPlugins.All, getVideoUrls());


    var i;
    var mediaCompatiblePlugins = Mediasite.Player.MediaPlugins.None;
    for (i = 0; i < calculated.length; i += 1) {
        mediaCompatiblePlugins = mediaCompatiblePlugins | calculated[i].pluginType;
    }

    return mediaCompatiblePlugins;
}

Mediasite.Player.getFirstRampRecieverAddress = function (presentation) {
    var i, j;
    var stream, videoUrl, receiverUrl;

    if (!presentation || !presentation.Streams) {
        return null;
    }

    for (i = 0; i < presentation.Streams.length; i += 1) {
        stream = presentation.Streams[i];
        for (j = 0; j < stream.VideoUrls.length; j += 1) {
            videoUrl = stream.VideoUrls[j];
            if (videoUrl && videoUrl.MulticastReceiverUrl) {
                return videoUrl.MulticastReceiverUrl;
            }
        }
    }

    return null;
};

Mediasite.Player.Overlay = function() {
    var loadingOverlay = $('.LoadingOverlay');
    var loadingMessage = $('.LoadingMessage');

    this.Show = function() {
        loadingOverlay.show();
    };

    this.Hide = function() {
        loadingMessage.fadeOut(1000, function() {
            loadingOverlay.hide();
        });
    };

    this.SetMessage = function(message) {
        loadingMessage.text(message);
    };
};

Mediasite.Player.PlayerStateManager = function () {
    var logger = Mediasite.Logging.LoggerFactory("PlayerState");

    var _events = new Mediasite.Player.EventBundle();
    var _eventNames = Mediasite.Player.PlayerStateManagerEvent;
    var self = this;

    var playerState = Mediasite.Player.PlayerState.NotReady;
    var liveState = Mediasite.Player.PresentationStatus.ScheduledForLive;
    var isLive = false;
    var isEndedLocked = false;

    var previousState;


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

    this.LiveStatusChange = function (status) {
        isLive = true;

        liveState = status;

        switch (status) {
            case Mediasite.Player.PresentationStatus.ScheduledForLive:
            case Mediasite.Player.PresentationStatus.OpenForLive:
                setPlayerState(Mediasite.Player.PlayerState.Waiting);
                fireEvent();
                break;
            case Mediasite.Player.PresentationStatus.Live:
                break;
            case Mediasite.Player.PresentationStatus.LivePaused:
                break;
            case Mediasite.Player.PresentationStatus.LiveEnded:
                setPlayerState(Mediasite.Player.PlayerState.Ended);
                fireEvent();
                break;
        }
    };

    this.MediaStateChange = function (state) {
        if (isLive) {
            switch (state) {
                case Mediasite.Player.MediaState.Ready:
                    setPlayerState(Mediasite.Player.PlayerState.InteractionRequired);
                    break;
                case Mediasite.Player.MediaState.Playing:
                    setPlayerState(Mediasite.Player.PlayerState.Playing);
                    break;
                case Mediasite.Player.MediaState.Paused:
                    setPlayerState(Mediasite.Player.PlayerState.Paused);
                    break;
                case Mediasite.Player.MediaState.Closed:
                case Mediasite.Player.MediaState.MediaEnded:
                case Mediasite.Player.MediaState.Stopped:
                    if (liveState == Mediasite.Player.PresentationStatus.LiveEnded) {
                        setPlayerState(Mediasite.Player.PlayerState.Ended);
                    }
                    else if (playerState == Mediasite.Player.MediaState.NotReady) {
                        break;
                    } else {
                        setPlayerState(Mediasite.Player.PlayerState.Stopped);
                    }
                    break;
                default:
                    setPlayerState(Mediasite.Player.PlayerState.NotReady);
                    break;
            }
        }
        else {
            switch (state) {
                case Mediasite.Player.MediaState.Ready:
                    setPlayerState(Mediasite.Player.PlayerState.InteractionRequired);
                    break;
                case Mediasite.Player.MediaState.Playing:
                    setPlayerState(Mediasite.Player.PlayerState.Playing);
                    break;
                case Mediasite.Player.MediaState.Paused:
                    setPlayerState(Mediasite.Player.PlayerState.Paused);
                    break;
                case Mediasite.Player.MediaState.Closed:
                case Mediasite.Player.MediaState.MediaEnded:
                case Mediasite.Player.MediaState.Stopped:
                    setPlayerState(Mediasite.Player.PlayerState.Ended);
                    break;
                default:
                    setPlayerState(Mediasite.Player.PlayerState.NotReady);
                    break;
            }
        }

        fireEvent();
    };

    this.MediaError = function (error) {
        console.log('MediaError', error);
        setPlayerState(Mediasite.Player.PlayerState.Error);
        fireEvent();
    };

    this.GetState = function () {
        return { State: playerState, IsLive: isLive };
    };

    function setPlayerState(state) {
        if (isEndedLocked) {
            return;
        }

        playerState = state;

        if (state == Mediasite.Player.PlayerState.Ended && isLive) {
            isEndedLocked = true;
        }
    }

    function fireEvent() {
        var currentState = self.GetState();
        var changed = !previousState 
            || previousState.State !== currentState.State
            || previousState.IsLive != currentState.IsLive;

        // Usually, when a presentation ends, the Paused state happens before the Ended state.
        // However, HTML5 playback in IE can reverse the order. Don't let a trailing Paused state 
        // cancel our Ended state. See BUG46101.
        var pauseAfterEnded = (currentState.State === Mediasite.Player.PlayerState.Paused) && previousState && (previousState.State === Mediasite.Player.PlayerState.Ended);

        if (changed && !pauseAfterEnded) {
            previousState = { State: currentState.State, IsLive: currentState.IsLive }; // clone for protection
            logger.Debug("State: " + currentState.State, "IsLive: " + currentState.IsLive);

            _events.fire(_eventNames.PlayerStateChanged, currentState);
        }
    }

};


Mediasite.Player.WindowInterface = function (concreteWindow) {
    if (!concreteWindow) {
        concreteWindow = window;
    }
    this._concreteWindow = concreteWindow;

    // Determine if concreteWindow.postMessage isn't available

    if (!concreteWindow.postMessage) {
        this._canPostMessage = false;
    }

    var ie = Mediasite.BrowserDetect.InternetExplorer();

    if (ie && ie <= 7 && concreteWindow.postMessage) {
        // The Legacy Recorder UI's WebView reports as IE7, but may actually support postMessage. 
        // We don't know for sure whether this particular system supports postMessage between 
        // separated windows or just between parent and child windows, so let's be conservative 
        // and assume the more restrictive. Accomplish this by pretending this is IE8.
        ie = 8; // Fake out!
    }

    if (ie && ie <= 7) {
        // IE 7 has no postMessage.
        this._canPostMessage = false;
    }
    if (ie && (8 <= ie && ie <= 9) && concreteWindow !== window.parent) {
        // IE 8 and 9 can postMessage, but only between iframe and host windows

        this._canPostMessage = false;
    }
    if (ie && ie <= 11 && Mediasite.BrowserDetect.Local() && concreteWindow !== window.parent) {

        // TODO: better feature/failure-sniff for window.postMessage
        // IE10 will successfully window.postMessage to
        // window (self), iframe, window.parent, window.opener, and window.open("url")
        
        // IE10 won't fail when window.postMessage on window.open("relativeFile") or window.parent
        // but the messaged window won't receive the message when running locally (file://...).
        // (An exception: if the page is on a network drive, the window *does* receive the message
        // but that's hard to sniff from JavaScript.)
        
        // IE11 is inconsistent. On some computers, it works fine on local. On other computers, no.

        this._canPostMessage = false;
    }

    if (!this._canPostMessage) {
		try {
			// For browsers which don't support postMessage (notably older IE),
			// we can attempt triggering fake events by reaching into the window
		
			var windowBody = this._concreteWindow.document;
			this._canAccessWindowProperties = true
		} catch (e) {
			// security restrictions, nevermind
		}
	}
};

Mediasite.Player.WindowInterface.prototype = {	
    addHandler: function (eventName, handler, usejQueryEvent) {
        if (!usejQueryEvent) {
            var originalHandler = handler;
            handler = function ($event) {
                originalHandler($event.originalEvent);
            };
        }

        $(this._concreteWindow).on(eventName, handler);
    },

    removeHandler: function (eventName, handler) {
        $(this._concreteWindow).off(eventName, handler);
    },

    _canPostMessage: true,
    _canAccessWindowProperties: false,

    postMessage: function (message, targetOrigin) {
        if (this._canPostMessage) {
            try {
                this._concreteWindow.postMessage(message, targetOrigin);
                return;
            } catch (e) {
                if (targetOrigin === '*') {
                    // IE 8/9 will successfully window.postMessage for iframe/window.parent
                    // but throws a security error for other windows.
                    // This should only happen if the "canPostMessage" UA checks are removed.

                    this._canPostMessage = false;
                }
            }
        }

        if (this._canAccessWindowProperties) {
            var $;
            try {
                $ = this._concreteWindow.$;
            } catch (e) {
                // whoops, security error we didn't catch earlier
            }

            if ($) {
                var messageEvent = $.Event("message");
                messageEvent.originalEvent = {
                    data: message,
                    source: window,
                    origin: "*"
                };

                var target = $(this._concreteWindow);
				
                this._concreteWindow.setTimeout(function () { // event must be async like postMessage
                    target.trigger(messageEvent);
                }, 1);
				
				return;
            }
        }
    },

    equals: function (concreteWindow) {
        return this._concreteWindow == concreteWindow;
    },

    equalsInterface: function (windowInterface) {
        return windowInterface.equals(this._concreteWindow);
    }
	
};



Mediasite.Player.WindowInterfaceIE11Popout = function() {};

Mediasite.Player.WindowInterfaceIE11Popout.prototype = {	
	postMessage: function (message, targetOrigin) {
		var $;
		try {
			$ = this._concreteWindow.$;
		} catch (e) {
			// whoops, security error we didn't catch earlier
		}

		if ($) {
			var messageEvent = $.Event("message");
			messageEvent.originalEvent = {
				data: message,
				source: window,
				origin: "*"
			};

			var target = $(this._concreteWindow);
			
			setTimeout(function () { // event must be async like postMessage
				target.trigger(messageEvent);
			}, 1);
			
			return;
		}
    }
};

Mediasite.Player.Memoization = function() {
	var self = this;
	var cache = {};
	
	// setValue(p1, .., pn)
	// Args p1 through pn are used to create a unique key, a concatenated string of the passed in args. 
	
	function generateCacheKey() {
		var returnString = "";
		
		for (var i = 0, j = arguments.length; i < j; i++){
			if(arguments[i]){
				returnString += 'p' + i + arguments[i].toString();
			} else {
				returnString += 'p' + 'undefined';
			}
		};
		return returnString;
	}	
	
	// setValue(p1, .., pn-1, pn)
	// Args p1 through pn-1 are used to create a unique key for caching the value
	// Arg pn is the last argument which contains the value to cache
	
	self.setValue = function() {
		var keyArgs = [];
		var keyString = "";
		
		for (var i = 0, j = arguments.length - 1; i < j; i++){
			keyArgs.push(arguments[i]);
		};
		keyString = generateCacheKey.apply(this, keyArgs);
		cache[keyString] = arguments[arguments.length - 1];
	};
	
	// setValue(p1, .., pn)
	// Args p1 through pn are used to create a unique key for retrieving the cached value

	self.getValue = function() {		
		return cache[generateCacheKey.apply(this, arguments)];
	};
	
	return self;	
};

Mediasite.Player.debounce = function debounce(func, wait, immediate) {
    var timeout;
    return function () {
        var context = this
        var args = arguments;
        var later = function () {
            timeout = null;
            if (!immediate) {
                func.apply(context, args);
            }
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) {
            func.apply(context, args);
        }
    };
};
