// StreamViewer
// +==================================================================
// Events: 
// ===================================================================
Mediasite.Player.StreamViewer = function (options) {
    var logger = Mediasite.Logging.LoggerFactory("StreamViewer:" + options.Stream.StreamType, { separator: "\n" });
    //var logger = { Debug: function() { }  };

    var currentTime;
    var lastSyncCheck = new Date();
    var self = this;

    var mainStreamPlaybackRate = 1;
    var canChangePlaybackRate = false;
    var shouldTweakUsingPlaybackRate = true;
    var recentPlaybackRateTweaks = [];
    var recentActionsExpireAfter = 20000; // milliseconds
    var tooManyTweaks = 4;


    //#region Public Methods
    //******************************************************************************
    this.Stream = options.Stream;
    this.StreamType = options.Stream.StreamType;
    this.IsMainStream = options.IsMainStream;
    this.MediaPlayerModel = options.MediaPlayerModel;
    this.SlideTracker = options.SlideTracker;
    this.StreamViewHandler = options.StreamViewHandler;
    this.CurrentMediaState = Mediasite.Player.MediaState.Undefined;
    this.CurrentTime = currentTime ? currentTime : 0;

    this.SetMainStreamPlaybackRate = function (value) {
        mainStreamPlaybackRate = value;
        // SyncCurrentTime is responsible for fixing/tweaking mediaPlayerModel.playbackRate to match this value
    };

    //Make sure we are the the specified time due to normal run time
    this.SyncCurrentTime = function (value, isLivePresentation) {
        if (currentTime == value) return;

        this.CurrentTime = currentTime = value;

        if (options.Stream.HasSlides()) {
            this.SlideTracker.UpdateCurrentSlide(currentTime);
        }


        //Sync videos OnDemand only
        if (!this.IsMainStream && this.MediaPlayerModel && !isLivePresentation) {

            //Sync When Paused
            //            if (this.CurrentMediaState != Mediasite.Player.MediaState.Playing) {
            //                this.MediaPlayerModel.SetPosition(currentTime);
            //                return;
            //            }

            //Sync When Playing (This strategy may not work for Smooth Streaming content.)
            var currentSyncTime = new Date();
            var diff = Math.abs(Math.round(currentSyncTime - lastSyncCheck));
            var checkSyncNow = (diff > 1000);                                                           //Check video sync every second

            if (checkSyncNow) {
                lastSyncCheck = currentSyncTime;

                // Detect excessive seeks/playback rate tweaks
                while (recentPlaybackRateTweaks.length && (currentSyncTime < recentPlaybackRateTweaks[0] - recentActionsExpireAfter)) {
                    // clear out stale events
                    recentPlaybackRateTweaks.shift();
                }
                if (recentPlaybackRateTweaks.length >= tooManyTweaks && shouldTweakUsingPlaybackRate) {
                    logger.Debug("*** Forbidding playback rate tweaks");

                    shouldTweakUsingPlaybackRate = false;
                }

                var streamPosition = this.MediaPlayerModel.GetPosition();
                var offset = Math.round(currentTime - streamPosition);

                if (Math.abs(offset) >= 1000) {
                    // Way out of sync.  Probably a seek on main stream, so seek on this stream to catch up
                    this.MediaPlayerModel.SetPosition(currentTime);
                    if (recentPlaybackRateTweaks.length && shouldTweakUsingPlaybackRate) {
                        logger.Debug("Secondary stream out of sync by: " + offset, "seeking to catch up");
                    }
                }
                else if (Math.abs(offset) >= 100 && canChangePlaybackRate && shouldTweakUsingPlaybackRate) {
                    // A little out of sync.  Speed up or slow down relative to the mandated playback rate to improve the sync
                    recentPlaybackRateTweaks.push(currentSyncTime);

                    var adjustByFactor = Math.min(Math.abs(offset) / 1000, 0.2);
                    adjustByFactor *= (Math.abs(offset) / offset);  // positive or negative

                    var syncPlaybackRate = mainStreamPlaybackRate * (1 + adjustByFactor);
                    var currentRate = Math.round(100 * this.MediaPlayerModel.GetPlaybackRate()) / 100;
                    if (currentRate != syncPlaybackRate) {
                        this.MediaPlayerModel.SetPlaybackRate(syncPlaybackRate);
                        logger.Debug("Secondary stream out of sync by: " + offset, "adjusting playback rate by " + adjustByFactor + " to " + syncPlaybackRate);
                    }
                }
                else {
                    // In sync

                    var currentRate = Math.round(100 * this.MediaPlayerModel.GetPlaybackRate()) / 100;
                    if (currentRate != mainStreamPlaybackRate) {
                        // Reset playback rate
                        this.MediaPlayerModel.SetPlaybackRate(mainStreamPlaybackRate);
                        logger.Debug("Secondary stream out of sync by: " + offset, "resetting playback rate to " + mainStreamPlaybackRate + " (currently " + currentRate + ")");
                    }
                }

                lastSyncCheck = new Date();
            }
        }
    };

    function addStateHandler() {
        if (self.MediaPlayerModel) {

            var stateChangedHandler =
            {
                StateChanged: function (state) {
                    self.CurrentMediaState = state;
                },
                MediaLoaded: function (mediaInfo) {
                    canChangePlaybackRate = mediaInfo.CanChangeSpeed;
                }
            };

            self.MediaPlayerModel.AddEventHandler(stateChangedHandler);
        }
    }

    //Method used to reset StreamViewer when PlayAsVideo is called on StreamViewerManager
    this.Reset = function (resetOptions) {

        //Destroy and remove Video Media Element
        if (this.MediaPlayerModel) {
            this.MediaPlayerModel = undefined;
        }

        //Destroy and remove Video Slide Tracker
        if (this.SlideTracker) {
            this.SlideTracker.Destroy();
            this.SlideTracker = undefined;
        }

        //Reset module variables
        this.CurrentMediaState = Mediasite.Player.MediaState.Undefined;
        this.IsMainStream = false;
        this.Stream = undefined;
        this.Stream = resetOptions.Stream;
        this.MediaPlayerModel = resetOptions.MediaPlayerModel;
        this.SlideTracker = resetOptions.SlideTracker;
        this.IsMainStream = resetOptions.IsMainStream;

        //Add state monitoring
        addStateHandler();

        if (this.MediaPlayerModel) {
            this.MediaPlayerModel.SetPlaybackRate(resetOptions.PlaybackRate);
        }

    };

    //******************************************************************************
    //#endregion

    // initialize
    (function () {
        addStateHandler();
    })();
};

Mediasite.Player.StreamOptions =
{
    VideoUrls: [],
    Slides: [],
    SlideBaseUrl: '',
    SlideImageFileNameTemplate: '',
    SlideThumbnailFileNameTemplate: '',
    AnonymousAllowed: false,
    ThumbnailMaxHeight: 100,
    SlidePlaybackTicketId: '',
    DisplayContent: Mediasite.ContentType.Video
};

Mediasite.Player.Stream = function (inputOptions) {

    var options = $.extend({}, Mediasite.Player.StreamOptions, inputOptions);
    this.StreamType = options.StreamType;
    this.IsPlaybackAudioSource = !!options.PlaybackAudioSource;
    this.VideoUrls = options.VideoUrls;
    this.Slides = options.Slides;
    this.AudioOnly = options.AudioOnly;
    this.AspectRatio = options.AspectRatio;
    this.Priority = options.Priority;
    this.ThumbnailUrl = options.ThumbnailUrl;
    this.LiveSlideQueue = [];
    this.HasSlideContent = options.HasSlideContent;  //Is the stream expected to have slides (typically for live)
    this.IsMainStream = options.IsMainStream;  //This is the stream that is driving the timeline (currently)
    this.DisplayContent = options.DisplayContent;
    this.SlideUrlProvider = new Mediasite.Player.SlideUrlProvider(options);

    this.HasVideo = function () {
        return this.VideoUrls.length > 0;
    };

    this.HasSlides = function () {
        return this.Slides.length > 0 || this.HasSlideContent;
    };

    function updateSlides() {
        $.each(self.Slides, function (index, slide) {
            self.Slides[index] = $.extend({}, new Mediasite.Slide(), slide);
            self.Slides[index].SetUrlProvider(self.SlideUrlProvider);
        });
    }

    function updateVideoUrls() {
        var i;

        $.each(self.VideoUrls, function (index, videoUrl) {
            if (videoUrl.CanChangeScheme === true && window.location.protocol !== videoUrl.Location.toLowerCase().substr(0, window.location.protocol.length)) {
                videoUrl.Location = window.location.protocol + videoUrl.Location.substr(videoUrl.Location.indexOf(':') + 1);
            }
        });
    }

    var self = this;

    // initialize
    (function () {
        updateSlides();
        updateVideoUrls();
    })();

};

Mediasite.Player.SlideUrlProvider = function(options) {

    var SlideBaseUrl = options.SlideBaseUrl;
    var SlideImageFileNameTemplate = options.SlideImageFileNameTemplate;
    var SlideThumbnailFileNameTemplate = options.SlideThumbnailFileNameTemplate;
    var AnonymousAllowed = options.AnonymousAllowed;
    var ThumbnailMaxHeight = options.ThumbnailMaxHeight;
    var SlidePlaybackTicketId = options.SlidePlaybackTicketId;

    //Stream Provider Methods
    this.GetSlideUrl = function (slideNumber, width, height) {

        slideNumber = slideNumber.toString();
        while (slideNumber.length < 4) {
            slideNumber = '0' + slideNumber;
        }

        var slideFileName = SlideImageFileNameTemplate.replace(new RegExp('\\{0(\\}|:\\w+\\})', 'gm'), slideNumber);
        if (height) {
            if (SlideThumbnailFileNameTemplate != '' && height <= ThumbnailMaxHeight) {
                slideFileName = SlideThumbnailFileNameTemplate.replace(new RegExp('\\{0(\\}|:\\w+\\})', 'gm'), slideNumber);
            }

            if (SlideThumbnailFileNameTemplate == '') {
                slideFileName = slideFileName.replace(/\./, "_" + width + "_" + height + ".");
            }
        }

        var source = SlideBaseUrl + slideFileName;
        if (AnonymousAllowed === false) {
            source = Mediasite.Player.ImageUrlPlayticketFormatter(source, SlidePlaybackTicketId)
        }

        return source;
    };

    this.GetCacheFriendlySlideUrl = function (slideNumber, height, aspect) {
        var size = Mediasite.Slide.getCacheFriendlyImageSize(height, aspect);
        if (size.width() > 0) {
            return this.GetSlideUrl(slideNumber, size.width(), size.height());
        } else {
            return this.GetSlideUrl(slideNumber);
        }
    };

};

Mediasite.Player.StreamViewerEvent = {
    SlideChange: 'SlideChange',
    AddNewSlide: 'AddNewSlide'
};

Mediasite.Player.SlideTracker = function (options) {

    var events;
    var eventNames;
    var slideTimingSearcher;
    var currentSlideNumber = 0;

    this.UpdateCurrentSlide = function (position) {


        var futureSlides = options.Stream.LiveSlideQueue;
        var slides = options.Stream.Slides;
        var addNewSlide = function (slide) {
            slides.push(slide);
            events.fire(eventNames.AddNewSlide, slide);
        };

        if (futureSlides.length > 0) {

            while (futureSlides[0] && futureSlides[0].Time <= position) {
                // We have a new slide within scope.

                var nextSlide = futureSlides.shift();
                var expectedNextNumber = slides.length + 1;
                var fakeSlide, fakeNumber, fakeTime;

                if (nextSlide.Number === expectedNextNumber) {
                    // nextSlide.Number is correct, so add it.
                    addNewSlide(nextSlide);
                } else if (nextSlide.Number > expectedNextNumber) {
                    // nextSlide.Number is too large, so add fake intermediate slides.

                    for (fakeNumber = expectedNextNumber; fakeNumber < nextSlide.Number; fakeNumber++) {
                        // Add a fake slide.
                        fakeTime = nextSlide.Time - (nextSlide.Number - fakeNumber);
                        fakeSlide = new Mediasite.Slide(fakeNumber, fakeTime, "", "");
                        addNewSlide(fakeSlide);
                        console.log('added fake slide ' + fakeNumber);
                    }

                    addNewSlide(nextSlide);
                } else if (nextSlide.Number < expectedNextNumber) {
                    // nextSlide.Number is too small. We should have already added a fake slide for this one.
                }
            }
        }

        var slide = getSlide(position);

        if (slide && (currentSlideNumber != slide.Number)) {
            currentSlideNumber = slide.Number;
            events.fire(eventNames.SlideChange, slide);
        }
    };

    this.UpdateMediaState = function (data) {
        if (data.State == Mediasite.Player.MediaState.MediaEnded) {
            currentSlideNumber = 0;
        }
        return false;
    };

    this.GetSlideAtPosition = function (position) {
        return getSlide(position);
    };

    this.Destroy = function () {
        events.removeAllHandlers();
        events = undefined;
        slideTimingSearcher = undefined;
        eventNames = undefined;
    };

    function getSlide(position) {

        var slide = slideTimingSearcher.GetCurrentItem(position);

        if (slide == undefined) {
            slide = new Mediasite.Slide(0, 0, "", "");
            slide.StreamType = options.Stream.StreamType;
        }

        return slide;
    }

    // initialize
    (function () {
        slideTimingSearcher = new Mediasite.Player.TimingSearcher(options.Stream.Slides);
        events = new Mediasite.Player.EventBundle();
        eventNames = Mediasite.Player.StreamViewerEvent;
        events.addHandler(options.EventHandlers);
    })();

};

Mediasite.Player.StreamViewerManager = function (streams, mediaPlayerModelOptions, mediaPlayerEventHandlers, slideTrackerEventHandlers, viewContext, playerModel, playerContext) {

    this.StreamViewerCollection = [];
    this.MainStreamViewer = undefined;
    this.CurrentPlaybackRate = 1.0;
    var self = this;
    var videoCount = 0;
    var videoLoadedCount = 0;
    var videoInfoCollection = [];
    var stateMonitorInterval;
    var videoReloadCount = 0;

    function loadStreamViewers() {

        detectStreams();

        $.each(streams, function (index, stream) {

            var dataStream = $.extend({}, stream);

            var streamViewHandlerOptions =
            {
                DisplayContent: dataStream.DisplayContent,
                StreamType: dataStream.StreamType,
                StreamOrdinal: dataStream.Priority,
                IsMainStream: dataStream.IsMainStream,
                VideoAvailable: dataStream.HasVideo(),
                StreamThumbnailUrl: dataStream.ThumbnailUrl,
                AspectRatio: dataStream.AspectRatio
            };

            var streamViewHandler = viewContext.getStreamViewHandler(streamViewHandlerOptions);
            if (streamViewHandler == undefined) return true;

            //Video
            var mediaPlayerModel = self.CreateMediaPlayerModel(dataStream, streamViewHandler);

            //Slides
            var slideTracker = self.CreateSlideTracker(dataStream);

            var streamViewerOptions =
            {
                Stream: dataStream,
                MediaPlayerModel: mediaPlayerModel,
                SlideTracker: slideTracker,
                StreamViewHandler: streamViewHandler,
                IsMainStream: dataStream.IsMainStream
            };

            var streamViewer = new Mediasite.Player.StreamViewer(streamViewerOptions);
            self.StreamViewerCollection.push(streamViewer);

            if (dataStream.IsMainStream) {
                self.MainStreamViewer = streamViewer;
            }

            return true;
        });
    }

    this.PlayAsVideo = function (streamType) {

        //BUG36830 - MSL
        //Special casing some code in this method for iOS6
        //When swapping videos and moving the video element ios6
        //has problems with the order of these events
        var ios6 = Mediasite.BrowserDetect.IOS6();


        //Stop Video & reset Module variables
        var mainMediaPlayerModel = self.MainStreamViewer.MediaPlayerModel;

        if (!mainMediaPlayerModel.SupportsPlayAsVideo()) {
            alert("Media element does not support Play As Video function.");
            return;
        }

        self.ExecuteMediaPlayerModelMethod('Pause');
        clearInterval(stateMonitorInterval);
        self.MainStreamViewer = undefined;
        videoCount = 0;
        videoLoadedCount = 0;
        videoInfoCollection = [];

        //Reset stream DisplayContent & Main Stream
        //Selected StreamType will be set to Main Stream
        $.each(streams, function (index, stream) {

            stream.IsMainStream = false;

            if (stream.StreamType == streamType) {
                stream.IsMainStream = true;
                stream.DisplayContent = Mediasite.ContentType.Video;
                videoCount++;
                return true;
            }

            if (stream.DisplayContent == Mediasite.ContentType.Video) {
                stream.DisplayContent = Mediasite.ContentType.None;

                if (stream.HasSlides()) {
                    stream.DisplayContent = Mediasite.ContentType.Slide;
                }
            }

            return true;
        });

        //Reset StreamViewers
        $.each(self.StreamViewerCollection, function (index, streamViewer) {

            var dataStream = streams[index];
            var streamViewHandler = streamViewer.StreamViewHandler;

            var mediaPlayerModel;
            if (dataStream.DisplayContent == Mediasite.ContentType.Video) {
                mediaPlayerModel = mainMediaPlayerModel;
                if (!ios6) {
                    var mediaWrapper = mediaPlayerModel.GetMediaWrapper();
                    mediaWrapper.appendTo(streamViewer.StreamViewHandler.MediaContainer);
                }

            }

            var streamViewerOptions = {
                Stream: dataStream,
                MediaPlayerModel: mediaPlayerModel,
                SlideTracker: self.CreateSlideTracker(dataStream),
                IsMainStream: dataStream.IsMainStream,
                PlaybackRate: self.CurrentPlaybackRate
            };

            streamViewer.Reset(streamViewerOptions);

            if (dataStream.IsMainStream) {
                playerContext.PlaybackOptions.StartPosition = streamViewer.CurrentTime;
                self.MainStreamViewer = streamViewer;
            }

            streamViewHandler.SetDisplayContent(dataStream.DisplayContent);

            return true;
        });

        stateMonitorInterval = setInterval(monitorMediaState, 200);

        //When executing the PlayAsVideo function, video URLs need to have a cache buster added to the querystring
        var clonedUrls = JSON.parse(JSON.stringify(self.MainStreamViewer.Stream.VideoUrls));

        $.each(clonedUrls, function (index, videoUrl) {
            if (videoUrl.Location.indexOf("?") > 0) {
                videoUrl.Location += "&param=" + new Date().getTime();
            } else {
                videoUrl.Location += "?param=" + new Date().getTime();
            }
        });

        self.MainStreamViewer.MediaPlayerModel.SetNewVideo(clonedUrls, self.MainStreamViewer.Stream.StreamType);

        if (ios6) {
            var mediaWrapper = mainMediaPlayerModel.GetMediaWrapper();
            mediaWrapper.appendTo(self.MainStreamViewer.StreamViewHandler.MediaContainer);
        }

    };

    function getPreferredAudioStreamType(streams) {
        var i, result;
        for (i = 0; i < streams.length; i++) {
            if (streams[i].IsPlaybackAudioSource) {
                result = streams[i].StreamType;
            }
        }

        if (typeof result !== 'undefined') {
            return result;
        } else {
            return getPreferredAudioStreamTypeWithoutProperty(streams);
        }
    }

    function getPreferredAudioStreamTypeWithoutProperty(streams) {
        var prefIndex, streamIndex, candidateStreamType, stream;
        var cst = Mediasite.ContentStreamType;
        var preferenceOrder = [cst.Video1, cst.Video2, cst.Video3, cst.Presentation];

        // Walk through each StreamType in order of preference.
        for (prefIndex = 0; prefIndex < preferenceOrder.length; prefIndex += 1) {
            candidateStreamType = preferenceOrder[prefIndex];

            // Look for a stream of matching StreamType.
            for (streamIndex = 0; streamIndex < streams.length; streamIndex += 1) {
                stream = streams[streamIndex];

                // For sanity, require that the stream actually have video.
                if (stream.StreamType === candidateStreamType && stream.VideoUrls.length > 0) {
                    return candidateStreamType;
                }
            }
        }

        // We didn't find a video stream.
        // This can't be good! (Or perhaps it's just player preview in Portal.)
        return '';
    }

    function isSingleVideoOnlyBrowser() {
        if (Mediasite.BrowserDetect.Blackberry()) {
            return true;
        }

        if (Mediasite.BrowserDetect.IOS() && !Mediasite.BrowserDetect.IOS10AndGreater()) {
            return true;
        }

        return false;
    }

    function detectStreams() {

        //Detect if Mobile
        var isMobile = Mediasite.BrowserDetect.Mobile();
        var singleVideoBrowser = isSingleVideoOnlyBrowser();
        var videoStreamCount = 0;
        var slideStreamCount = 0;
        var hasSlideStream = false;

        var preferredAudioStreamType = getPreferredAudioStreamType(streams);
        var preferredAudioStream;
        var highestPriorityVideoStream;

        $.each(streams, function (index, stream) {

            stream.IsMainStream = false;

            if (stream.VideoUrls.length > 0) {

                // Highest priority video stream will be main stream if we can only play one video at a time (old mobile).
                if (videoStreamCount == 0) {
                    highestPriorityVideoStream = stream;
                }

                if (stream.StreamType === preferredAudioStreamType) {
                    preferredAudioStream = stream;
                }

                videoStreamCount++;
            }

            if (stream.Slides.length > 0) {
                slideStreamCount++;
            }
            if (stream.StreamType == Mediasite.ContentStreamType.Slide) {
                hasSlideStream = true;
            }

        });

        if (singleVideoBrowser) {
            // If we can only play one video (old mobile), show the highest priority stream.
            if (highestPriorityVideoStream) {
                highestPriorityVideoStream.IsMainStream = true;
            }
        } else {
            // However, if we're playing multiple videos, make our preferred audio stream the 
            // driver so that its audio is syncronized best. Eventually, we will separate the 
            // audio driver from the timeline driver... but not today.
            if (preferredAudioStream) {
                preferredAudioStream.IsMainStream = true;
            }
        }

        if (viewContext.contentCapability == Mediasite.Player.ContentCapability.VideoAndSlides || singleVideoBrowser) {

            $.each(streams, function (index, stream) {

                stream.DisplayContent = Mediasite.ContentType.None;

                if (stream.IsMainStream) {
                    stream.DisplayContent = Mediasite.ContentType.Video;
                    return true;
                }

                if (stream.VideoUrls.length > 0) {

                    if (stream.Slides.length == 0) {
                        stream.DisplayContent = Mediasite.ContentType.None;
                    } else {
                        stream.DisplayContent = Mediasite.ContentType.Slide;
                    }

                    return true;
                };

                if (stream.StreamType == Mediasite.ContentStreamType.Slide) {
                    stream.DisplayContent = Mediasite.ContentType.Slide;
                    return true;
                }

                return true;
            });
        }
        else {

            $.each(streams, function (index, stream) {

                stream.DisplayContent = Mediasite.ContentType.None;

                if (stream.VideoUrls.length) {
                    stream.DisplayContent = Mediasite.ContentType.Video;
                    return true;
                };

                if (stream.StreamType == Mediasite.ContentStreamType.Slide) {
                    stream.DisplayContent = Mediasite.ContentType.Slide;
                    return true;
                }

                return true;
            });
        }
    }
    
    this.CreateMediaPlayerModel = function (stream, streamViewHandler) {

        var pc = playerContext;

        var mediaPlayerModel = undefined;
        if (stream.DisplayContent == Mediasite.ContentType.Video) {
            videoCount++;
            var streamOptions = $.extend({}, mediaPlayerModelOptions);
            streamOptions.mediaContainer = streamViewHandler.MediaContainer;

            streamOptions.disableVolume = !stream.IsMainStream;
            streamOptions.streamType = stream.StreamType;
            streamOptions.isMainStream = stream.IsMainStream;

            var strat = stream.playbackStrategies[0];

            streamOptions.selectedMediaPlayerType = Mediasite.Player.MediaPlayerType.FromPluginType(strat.pluginType);
            streamOptions.selectedVideoSource = strat.source;

            var convivaConfigurationFactory = new Mediasite.Player.ConvivaConfiguration.ConvivaConfigurationFactory();

            var partialConvivaConfig = convivaConfigurationFactory.getPartialConvivaConfiguration(playerContext);
            streamOptions.getConvivaConfiguration = convivaConfigurationFactory.makeGetConvivaConfiguration(partialConvivaConfig);

            mediaPlayerModel = Mediasite.MediaPlayer.CreateModel(streamOptions);
            mediaPlayerModel = $.extend({}, new Mediasite.MediaPlayer.BaseModel(), mediaPlayerModel);

            if (stream.IsMainStream) {
                mediaPlayerModel.AddEventHandler($.extend({}, mediaPlayerEventHandlers.CommonHandlers, mediaPlayerEventHandlers.MainHandlers));
            } else {
                mediaPlayerModel.AddEventHandler(mediaPlayerEventHandlers.CommonHandlers);
            }
        }

        return mediaPlayerModel;
    };

    this.CreateSlideTracker = function (stream) {
        var slideTracker = undefined;
        if (stream.HasSlides()) {
            var slideTrackerOptions =
            {
                Stream: stream,
                EventHandlers: slideTrackerEventHandlers
            };

            slideTracker = new Mediasite.Player.SlideTracker(slideTrackerOptions);
        }

        return slideTracker;
    };


    this.ExecuteMediaPlayerModelMethod = function (functionName, data) {

        var mainOnlyMethods =
        {
            InactivityExceeded: '',
            UpdateOptions: '',
            SetPosition: '',
            RetrieveBookmark: '',
            SetAudioLanguage: '',
            SetVolume: '',
            ToggleMute: '',
            StepVolumeUp: '',
            StepVolumeDown: '',
            SkipForward: '',
            SkipBack: ''
        };
        var executeInMainOnly = mainOnlyMethods.hasOwnProperty(functionName);

        $.each(self.StreamViewerCollection, function (index, streamViewer) {
            if (streamViewer.IsMainStream || !executeInMainOnly) {

                if (streamViewer.MediaPlayerModel && streamViewer.MediaPlayerModel[functionName]) {
                    if (functionName == 'SetupMediaElement') {
                        streamViewer.MediaPlayerModel[functionName](streamViewer.Stream.VideoUrls);
                    } else {
                        streamViewer.MediaPlayerModel[functionName](data);
                    }
                }
            }
        });
    };

    this.ExecuteSlideTrackerMethod = function (functionName, data) {
        var slide;

        if (functionName == "UpdateMediaState") {
            $.each(self.StreamViewerCollection, function (index, streamViewer) {
                if (streamViewer.SlideTracker) {
                    streamViewer.SlideTracker[functionName](data);
                }
            });
        } else {
            $.each(self.StreamViewerCollection, function (index, streamViewer) {
                if (streamViewer.SlideTracker && data.StreamType == streamViewer.StreamType) {
                    slide = streamViewer.SlideTracker[functionName](data);
                    return false;
                }
            });
        }

        return slide;
    };

    this.UpdateStreamViewerPosition = function (position, isLivePresentation) {
        //Tell Streams the current position
        $.each(self.StreamViewerCollection, function (index, streamViewer) {
            streamViewer.SyncCurrentTime(position, isLivePresentation);
        });
    };

    this.UpdateCurrentSlide = function (slide) {
        $.each(self.StreamViewerCollection, function (index, streamViewer) {
            if (streamViewer.Stream.StreamType == slide.StreamType) {
                var slideUrlProvider = streamViewer.Stream.SlideUrlProvider;
                if (!streamViewer.Stream.AspectRatio) { // if the aspect ratio is zero or null or undefined.
                    calculateFirstSlideAspect(streamViewer.Stream, function (aspectRatio) {
                        streamViewer.Stream.AspectRatio = aspectRatio;
                        streamViewer.StreamViewHandler.SetContentAspectRatio(streamViewer.Stream.AspectRatio);
                    });
                }
                streamViewer.StreamViewHandler.UpdateCurrentSlide(slide, slideUrlProvider);
            }
        });
    };

    function calculateFirstSlideAspect(stream, doWithAspectRatio) {
        var minAspect = 4 / 3;
        var maxAspect = 16 / 9;
        var imageSizeProvider;
        var slideUrl;

        if (!stream.Slides || stream.Slides.length < 1) {
            doWithAspectRatio(0); // Translates to "I still don't know."
        }

        slideUrl = stream.SlideUrlProvider.GetSlideUrl(1);

        imageSizeProvider = new Mediasite.Player.ImageSizeProvider();
        imageSizeProvider.getSize(slideUrl, function (size) {
            var aspect = 0;
            if (size && size.height() > 0) {
                aspect = size.width() / size.height();
                aspect = Math.min(Math.max(aspect, minAspect), maxAspect);
            }
            doWithAspectRatio(aspect);
        });

    }

    this.AddNewSlide = function (slide) {
        // TODO: It looks like this calls StreamViewHandler.UpdateCurrentSlide(...) for all StreamViewers.
        //       Won't this cause unexpected behavior when multiple streams have slide decks?
        $.each(self.StreamViewerCollection, function (index, streamViewer) {
            if (streamViewer.Stream.StreamType == slide.StreamType) {
                var slideUrlProvider = streamViewer.Stream.SlideUrlProvider;
                streamViewer.StreamViewHandler.AddNewSlide(slide, slideUrlProvider);
            }
        });
    };

    this.IsMediaLoaded = function (mediaInfo) {

        videoLoadedCount++;
        videoInfoCollection.push(mediaInfo);

        if (videoLoadedCount == videoCount) {
            var mediaData = {};
            mediaData.duration = mediaInfo.Duration;
            if (viewContext.contentCapability == Mediasite.Player.ContentCapability.VideoAndSlides) {

                $.each(videoInfoCollection, function (index, item) {

                    if (item.IsMainStream) {
                        mediaData.videoInfo = item;
                    }
                });

            } else {
                mediaData.videoInfo = videoInfoCollection;
            }

            return mediaData;
        }

        return undefined;
    };

    function monitorMediaState() {

        if (!self.MainStreamViewer) return;

        var mainStreamMediaState = self.MainStreamViewer.CurrentMediaState;
        $.each(self.StreamViewerCollection, function (index, streamViewer) {
            if (!streamViewer.IsMainStream && streamViewer.MediaPlayerModel) {

                if (mainStreamMediaState == Mediasite.Player.MediaState.MediaEnded || streamViewer.CurrentMediaState == Mediasite.Player.MediaState.MediaEnded) {
                    streamViewer.MediaPlayerModel.Stop();
                } else {
                    if (streamViewer.CurrentMediaState != mainStreamMediaState) {

                        if (mainStreamMediaState == Mediasite.Player.MediaState.Playing) {
                            streamViewer.MediaPlayerModel.Play();
                        }

                        if (mainStreamMediaState == Mediasite.Player.MediaState.Paused) {
                            streamViewer.MediaPlayerModel.Pause();
                        }

                        if (mainStreamMediaState == Mediasite.Player.MediaState.Stopped) {
                            streamViewer.MediaPlayerModel.Stop();
                        }
                    }
                }
            }
        });
    }

    this.UpdatePlaybackRate = function (value) {
        this.CurrentPlaybackRate = value;
        $.each(self.StreamViewerCollection, function (index, streamViewer) {
            streamViewer.SetMainStreamPlaybackRate(value);
        });
    };

    // initialize
    (function () {
        loadStreamViewers();
        stateMonitorInterval = setInterval(monitorMediaState, 200);
    })();
};

Mediasite.Player.StreamViewHandler = function (options) {
    var noop = function () { };

    this.MediaContainer = options.MediaContainer || noop;
    this.UpdateCurrentSlide = options.UpdateCurrentSlide || noop;
    this.AddNewSlide = options.AddNewSlide || noop;
    this.SetDisplayContent = options.SetDisplayContent || noop;
    this.SetContentAspectRatio = options.SetContentAspectRatio || noop;
};
