// Copyright 2007-2010 Panopto, Inc.
// All rights reserved.  Reuse and redistribution strictly prohibited.

// globals
var g_urlDeliveryInfo = "DeliveryInfo.aspx";

var g_urlNoteTogglePublic = "Notes/TogglePublic.aspx";
var g_urlNoteSubmit = "Notes/Note_Create.aspx";
var g_urlNoteUpdate = "Notes/Note_Update.aspx";
var g_urlNoteDelete = "Notes/Note_Delete.aspx";

var g_urlSearchResults = "Search/Results.aspx";

var g_urlImage = "Image.aspx";
var g_urlThumb = "Thumb.aspx";

var g_dMinViewerHeight = 500;
var g_dMinObjectWindowHeight = 600;
var g_dMinViewerWidth_Object = 930;
var g_dMinViewerWidth_NoObject = 700;
var g_dMinViewerWidth = g_dMinViewerWidth_Object;
var g_dScrollbarHeight = 17;

var g_fLeftPaneWidthPerc = .37;
var g_fRightPaneWidthPerc = .63;

var g_dBorderWidth = 1;

var g_dMinEventViewerHeight = 200;
var g_dMinEventViewerWidth = 340;

var g_fOffsetThreshold = 5.0;
var g_SyncRate = 500;

var g_dThumbnailHeight = 218;
var g_dThumbnailWidth = 250;

var g_dQuestionsHeight = 110;

var g_dContainerSpacing = 10;

// Global reference to viewer object for media player event handler access.
var g_pViewer;

function PanoptoViewer(el, viewerParams)
{
    var m_el = el;  // our root element
    
    // member controls
    var m_pVideo = null;          // presenter video stream
    var m_pEventTabViewer = null; // table of contents control
    var m_pTabViewer = null;      // object region with tab control to switch between streams
    var m_pObjectVideo = null;    // object video player
    var m_pThumbnails = null;     // thumbnail view
    var m_pQuestions = null;      // question entry for broadcast

    // our delivery info object - parses xml and holds our data
    var m_pDelivery = null;

    // true if there are object streams to show
    var m_bHasObjectRegion = true;
    var m_bObjectRegionMaximized = false;

    var m_bHasTimestamps = true;
    var m_bShowThumbnails = true;

    // timer to update streams in broadcast viewer
    var m_pDeliveryInfoTimer;

    // timer used to update event streams and synchronize videos in non-broadcast case
    var m_pSyncTimer;

    // lastsetvideoposition keeps track of the last place we manually positioned the video
    // we need this to get around a bug where the video player sets its position slightly before
    // the specified time which results in the display of incorrect events
    var m_fLastSetVideoPosition = 0;

    // SCORM variables. We break the delivery into segments and track which ones the user has watched.
    this.m_isScorm = viewerParams.IsScorm;  // True if we need to report back to a scorm lms
    this.m_scormProgress = null;            // Array<char>. Each represents a segment. ' ' if not watched or 'x' if watched.
    this.m_numScormSegmentsWatched = null;  // The number of segments the user has watched
    this.m_lengthOfScormSegment = null;     // The length of each segment.

    // the version of the system
    this.version = viewerParams.Version;

    // the delivery publicID
    this.deliveryID = viewerParams.DeliveryPublicID;

    // the session public id
    this.sessionPID;

    // the invocation id
    this.invocationId;

    // whether the session is being broadcast live.  set in SetDelivery()
    this.isLive;

    // whether the viewer is standalone notes
    this.isStandaloneNotes = false;

    // the user whose notes will be shown in the notes tab (for public notes).  null -> default to user's login.
    this.notesUser = null;

    // indicates whether the current user is permitted to make their personal notes stream public.
    // setting takes into account session group setting for viewers and current user's creator status.
    // set in SetDelivery()
    this.bAllowPublishNotes;

    // for login triggered from notes user <select>
    this.loginURL = viewerParams.LoginURL;

    this.playerXapLocation = viewerParams.PlayerXapLocation;

    this.startTime = viewerParams.StartTime;

    this.embedURL = viewerParams.EmbedURL;

    // initialize the viewer root and hosted controls
    // this will leak if called multiple times!
    this.Initialize = function Initialize() 
    {
        // create the events tab viewer
        m_pEventTabViewer = new EventTabViewer(document.getElementById("eventViewerDiv"), this);

        // create our thumbnails display
        m_pThumbnails = new Thumbnails(document.getElementById("thumbnails"), this);

        // create our tab viewer (boolean specifies large tabs)
        m_pTabViewer = new TabViewer(document.getElementById("tabViewer"), this, true);

        // create the questions entry
        m_pQuestions = new Questions(document.getElementById("questions"), this);

        // set event handler for window resize and clicking the thumbnail toggle button
        window.onresize = Function.createDelegate(this, this.Resize);
    }
    
    // get the data for the supplied delivery ID, then reconfigure the viewer
    // to display it.
    this.OpenDelivery = function OpenDelivery()
    {
        function deliveryCallback(pDocument, bSuccess)
        {
            var errorMessage = null;
            var loginRedirect = null;

            if (!pDocument || !bSuccess)
            {
                errorMessage = "Error connecting to server. Please try refreshing the page later.";
            }
            else
            {
                errorMessage = SelectSingleNodeValue(pDocument, "ErrorMessage");
                loginRedirect = SelectSingleNodeValue(pDocument, "LoginRedirect");
            }

            if (loginRedirect == "true")
            {
                window.location = viewerParams.LoginURL;
            }
            else if (errorMessage)
            {
                showMessage(errorMessage);

                // Stop pinging DeliveryInfo when broadcast ends
                if (m_pDeliveryInfoTimer)
                {
                    clearInterval(m_pDeliveryInfoTimer);
                }
            }
            else
            {
                // the name of the logged in user, for notes
                this.userName = SelectSingleNodeValue(pDocument, "UserName");
                this.bAllowPublishNotes = SelectSingleNodeValue(pDocument, "AllowPublicNotes");
                this.folderRole = SelectSingleNodeValue(pDocument, "FolderRole");
                this.invocationId = SelectSingleNodeValue(pDocument, "InvocationId");

                var deliveryInfo = SelectSingleNode(pDocument, "Delivery");
                var pDelivery = new DeliveryInfo(deliveryInfo);

                // Apparently function calls from delegates do not preserve the instance pointer.
                SetDelivery.call(this, pDelivery);
            }
        }

        var params =
        {
            deliveryID: viewerParams.DeliveryPublicID,
            tid: viewerParams.InviteTokenID,
            invocationID: this.invocationId
        };

        CreateRequest(g_urlDeliveryInfo, params, Function.createDelegate(this, deliveryCallback));
    }

    // Initializes the variables used for tracking a user's progress.
    this.InitializeScormData = function (fDeliveryLength) {
        if (this.m_isScorm) {
            // We will have a max of 100 segments
            this.m_numScormSegments = Math.min(Math.ceil(fDeliveryLength), 100);
            this.m_lengthOfScormSegment = fDeliveryLength / this.m_numScormSegments;
            this.m_numScormSegmentsWatched = 0;

            try {
                // Get the scrom progress data from the LMS
                var bookmark = SD.GetBookmark();
                this.m_scormProgress = new Array(this.m_numScormSegments);

                if (bookmark.length != this.m_numScormSegments) {
                    // The delivery doesn't match up with the bookmark ignore it
                    for (var i = 0; i < this.m_numScormSegments; i++) {
                        this.m_scormProgress[i] = ' ';
                    }
                }
                else {
                    // Parse the bookmark into the scormProgress array;
                    for (var i = 0; i < bookmark.length; i++) {
                        this.m_scormProgress[i] = bookmark.charAt(i);
                        if (this.m_scormProgress[i] == 'x') {
                            this.m_numScormSegmentsWatched++;
                        }
                    }
                }
            }
            catch (err) {
                // error contacting the scorm server. Just give up
            }
        }
    };

    // Updates a user's progress
    this.UpdateScormProgress = function (fVidPos) {
        if (this.m_isScorm && this.m_scormProgress != null) {
            var currentSegment = Math.floor(fVidPos / this.m_lengthOfScormSegment);
            // if fVidPos is past the end, just update the last segment
            currentSegment = Math.min(this.m_scormProgress.length - 1, currentSegment);

            if (this.m_scormProgress[currentSegment] != 'x') {
                this.m_scormProgress[currentSegment] = 'x';
                this.m_numScormSegmentsWatched++;

                if (this.m_numScormSegmentsWatched % 10 == 0 || this.m_numScormSegmentsWatched / this.m_scormProgress.length > .9) {
                    try {
                        // Collapse m_scormProgress back into a string and save as a bookmark
                        SD.SetBookmark(this.m_scormProgress.join(""));

                        // Check if we have finished
                        if (this.m_numScormSegmentsWatched / this.m_scormProgress.length > .9) {
                            SD.SetReachedEnd();
                        }

                        // Commit the data to the server
                        SD.CommitData();
                    }
                    catch (err) {
                        // error contacting the scorm server. Just give up
                    }
                }
            }
        }
    };

    // load information from the specified delivery into our viewers
    function SetDelivery(pDelivery)
    {
        // First load or broadcast hasn't started yet.
        if (m_pDelivery == null)
        {
            // set the page title
            document.title = pDelivery.sessionName + " - Panopto Viewer";

            this.isLive = pDelivery.isLive;

            // the display names and bios of creators who contributed to the session
            this.contributors = pDelivery.Contributors;
            this.publicNotesUsers = pDelivery.PublicNotesStreams;

            this.sessionPID = pDelivery.sessionPID;

            // initialize viewer components
            this.Initialize();

            // show the viewer
            showViewer(true);

            // Set folder name in top bar.
            var folderNameElement = document.getElementById("courseTag");
            folderNameElement.title = pDelivery.sessionGroupLongName;
            SetText(folderNameElement, pDelivery.sessionGroupLongName);

            // Show colon separator
            var nameDividerElement = document.getElementById("nameDivider");
            nameDividerElement.style.display = "block";

            // Set session name in top bar.
            var sessionNameElement = document.getElementById("sessionName");
            sessionNameElement.title = pDelivery.sessionName;
            SetText(sessionNameElement, pDelivery.sessionName);

            if (pDelivery.isStarted)
            {
                // Hide broadcast not started message (if shown) and display viewer div.
                showMessage(false);

                SetText(document.getElementById("infoSessionName"), pDelivery.sessionName);

                // Populate "share", "edit" and "manage" links on Info tab for Creator+
                if ((this.folderRole == Panopto.Data.AclRoleType.getName(Panopto.Data.AclRoleType.Creator))
                    || (this.folderRole == Panopto.Data.AclRoleType.getName(Panopto.Data.AclRoleType.Videographer))
                    || (this.folderRole == Panopto.Data.AclRoleType.getName(Panopto.Data.AclRoleType.Admin)))
                {
                    // Show DIV with links
                    document.getElementById("infoLinks").style.display = "block";

                    // Set HREFs for links based on delivery PublicID

                    document.getElementById("shareLink").href =
                        viewerParams.ApplicationPath + "/Pages/Sessions/List.aspx?id=" + this.deliveryID
                        + "&modalPage=SessionShare";

                    document.getElementById("editLink").href =
                        viewerParams.ApplicationPath + "/Pages/Editor/Default.aspx?id=" + this.deliveryID;

                    document.getElementById("manageLink").href =
                        viewerParams.ApplicationPath + "/Pages/Sessions/List.aspx?id=" + this.deliveryID
                        + "&modalPage=SessionManage";
                }

                var el_sessionGroupName = document.getElementById("infoSessionGroupName");
                var sessionGroupName = pDelivery.sessionGroupLongName;
                SetText(el_sessionGroupName, sessionGroupName);

                var el_sessionGroupLink = document.getElementById("infoSessionGroupLink");
                el_sessionGroupLink.href = viewerParams.ApplicationPath + "/Pages/Sessions/List.aspx?folderID=" + pDelivery.sessionGroupPID;

                var el_sessionAbstract = document.getElementById("sessionAbstract");
                SetTextWithLinks(el_sessionAbstract, pDelivery.sessionAbstract);
                SetTextWithNewlineTranslation(el_sessionAbstract, el_sessionAbstract.innerHTML);

                var el_sessionGroupAbstract = document.getElementById("sessionGroupAbstract");
                SetTextWithLinks(el_sessionGroupAbstract, pDelivery.sessionGroupAbstract);
                SetTextWithNewlineTranslation(el_sessionGroupAbstract, el_sessionGroupAbstract.innerHTML);

                // show the questions tab if this delivery is live
                m_pQuestions.SetVisible(this.isLive);

                // render our event tab viewer and load the first event
                if (m_pEventTabViewer)
                {
                    m_pEventTabViewer.RenderContents(pDelivery.arrTOCEvents, pDelivery.arrTranscriptEvents);
                }

                // render the thumbnails
                if (m_pThumbnails)
                {
                    // if we have thumbnails to show, render them - otherwise hide the thumbnail pane
                    var thumbEl = document.getElementById("thumbnails");
                    m_bShowThumbnails = m_bHasTimestamps = (pDelivery.arrTOCEvents.length > 0);
                    if (m_bHasTimestamps)
                    {
                        // don't show empty primary events in the thumb strip
                        var validEvents = new Array();
                        for (var i = 0; i < pDelivery.arrTOCEvents.length; i++)
                        {
                            if (pDelivery.arrTOCEvents[i].EventTargetType != "Primary")
                            {
                                validEvents.push(pDelivery.arrTOCEvents[i]);
                            }
                        }

                        m_pThumbnails.RenderContents(validEvents, pDelivery.sessionPID);
                        SetVisible(thumbEl, true);
                    }
                    else
                    {
                        SetVisible(thumbEl, false);
                    }
                }

                // Primary (top left) video player

                // Silverlight enabled
                if (g_bUsingSilverlight)
                {
                    // Variable speed playback only works if we switch to progressive
                    // http and seeking doesn't work with progressive http on WebKit
                    // based browsers because byte range requests don't work.
                    var enableVariableSpeedPlayback =
                        !g_bIsWebKit && 
                        (null != pDelivery.archivalStream.HttpUrl);

                    if (enableVariableSpeedPlayback && !Silverlight.isInstalled("5.0"))
                    {
                        // This can support variable speed playback, but SL5 is
                        // missing, so display the SL5 download message
                        enableVariableSpeedPlayback = false;
                        document.getElementById("sl5DownloadDiv").className = "visible";
                    }

                    // SSF player
                    if ((pDelivery.archivalStream.ViewerMediaFileType == "ism") || enableVariableSpeedPlayback)
                    {
                        // Use the SSF player if variable speed playback is 
                        // enabled because the server doesn't log progressive
                        // http and the SSF player has built in logging
                        m_pVideo = new SSFPlayer(document.getElementById("videoPlayerDiv"), this, true);

                        if (enableVariableSpeedPlayback)
                        {
                            var playbackRateDiv = document.getElementById("playbackRateControlsDiv");
                            playbackRateDiv.className = "visible";

                            var playbackRateSelect = document.getElementById("playbackRateSelect");
                            playbackRateSelect.onchange = function (e)
                            {
                                m_pVideo.SetPlaybackRate(playbackRateSelect);
                                if (null != m_pObjectVideo)
                                {
                                    m_pObjectVideo.SetPlaybackRate(playbackRateSelect);
                                }
                            }
                        }
                    }
                    // Legacy SL player
                    else
                    {
                        m_pVideo = new LegacySilverlightPlayer(document.getElementById("videoPlayerDiv"), this);
                    }
                }
                // Punt to embed, WMP can't handle ISM
                //BUGBUG: Consider secondary streams for futureproofness?
                else if (pDelivery.archivalStream.ViewerMediaFileType == "ism")
                {
                    var silverlightDownload = document.getElementById("silverlightDownload");
                    silverlightDownload.style.display = "block";

                    // Populate embed
                    document.getElementById("embedCell").innerHTML = this.embedURL;

                    // Hide main viewer
                    document.getElementById("viewer").style.display = "none";

                    return;
                }
                // Instantiate WMP player
                else
                {
                    m_pVideo = new VideoPlayer(document.getElementById("videoPlayerDiv"), "archivalPlayer", true);
                }

                this.InitializeScormData(pDelivery.Duration);

                // setup playback
                // Can't act on events for broadcast case, set callbacks to null  (BUGBUG: unless we're using SMF)
                var posChangedCallback = (pDelivery.isLive) ? null : Function.createDelegate(this, OnVideoPositionChanged);
                var playStateChangedCallback = (pDelivery.isLive) ? null : Function.createDelegate(this, OnPlayStateChanged);
                m_pVideo.Initialize(pDelivery.archivalStream, posChangedCallback, playStateChangedCallback);

                // render the object material region and load the first event
                if (m_pTabViewer)
                {
                    m_pTabViewer.SetViews(pDelivery.arrObjectViews);
                }

                // reflow based on player.
                OnResize();

                m_pDelivery = pDelivery;

                // seek to the requested spot in the video
                if (this.startTime)
                {
                    this.SetVideoPosition(this.startTime);
                }
            }
            else
            {
                showMessage("Broadcast has not started yet.  The viewer will load when content is available.");
            }
            
            // Reload delivery info every 10sec in broadcast mode to pick up new streams (or to pick up a broadcast that has just begun)
            if (pDelivery.isLive)
            {
                // We may hit this point again if we're waiting for a broadcast to begin.
                if (!m_pDeliveryInfoTimer)
                {
                    m_pDeliveryInfoTimer = setInterval(Function.createDelegate(this, this.OpenDelivery), 10000);
                }
            }

            // we will hit this multiple times while waiting for a broadcast to begin.  wrap with an if-check.
            if (!m_pSyncTimer)
            {
                m_pSyncTimer = setInterval(Function.createDelegate(this, Synchronize), g_SyncRate);
            }
        }
        // Broadcast update, refresh streams
        else
        {
            if (m_pTabViewer)
            {
                m_pTabViewer.SetViews(pDelivery.arrObjectViews);
            }
        }
    }

    this.GetHasObjectRegion = function () { return m_bHasObjectRegion; };

    this.SetHasObjectRegion = function(hasObjectRegion)
    {
        m_bHasObjectRegion = hasObjectRegion;
        m_pTabViewer.SetVisible(hasObjectRegion);

        var viewer = document.getElementById("viewer");
        var leftPane = document.getElementById("leftPane");
        var rightPane = document.getElementById("rightPane");
        var eventViewerDiv = document.getElementById("eventViewerDiv");

        if (hasObjectRegion)
        {
            // Show right pane and move event viewer below video
            leftPane.style.width = "37%";
            rightPane.style.display = "block";
            eventViewerDiv.style.position = "relative";
            eventViewerDiv.style.width = "";
            eventViewerDiv.style.paddingTop = "10px";

            g_dMinViewerWidth = g_dMinViewerWidth_Object;
        }
        else
        {
            // Collapse right pane and move event viewer to right of video
            leftPane.style.width = "100%";
            rightPane.style.display = "none";
            eventViewerDiv.style.position = "absolute";
            eventViewerDiv.style.paddingTop = "0px";

            g_dMinViewerWidth = g_dMinViewerWidth_NoObject;
        }

        viewer.style.minWidth = g_dMinViewerWidth + "px";

        this.Resize();
    }
    
    // enlarge / reduce object material tabviewer
    this.ToggleObjectRegionMaximized = function(maximized)
    {
        if (maximized != null)
        {
            m_bObjectRegionMaximized = maximized;
        }
        else
        {
            m_bObjectRegionMaximized = !m_bObjectRegionMaximized;
        }

        OnResize();
    }

    // Set thumbnail mode (show / hide).
    // Thumbnail display also depends on object region mode and presence of timestamps.
    function ToggleThumbnails(bShowThumbnails)
    {
        if (bShowThumbnails != null)
        {
            m_bShowThumbnails = bShowThumbnails;
        }
        else
        {
            m_bShowThumbnails = !m_bShowThumbnails;
        }
        
        m_pThumbnails.SetVisible(m_bShowThumbnails);
    }
    this.ToggleThumbnails = ToggleThumbnails;

    function OnVideoPositionChanged(fVidPos, bUserInitiated)
    {
        // Video position is not valid for _legacy_ broadcast, continue to ignore so we don't
        // introduce a regression.
        if (this.isLive && 
            this.m_pDelivery &&
            this.m_pDelivery.archivalStream &&
            this.m_pDelivery.archivalStream.ViewerMediaFileType == "wmv") return;

        // if this was triggered by a user action or the video player hasn't been initialized yet, then keep track of the last set position
        if (bUserInitiated || !m_pVideo)
        {
            m_fLastSetVideoPosition = fVidPos;
        }

        // determine the current item
        if (m_pDelivery && m_pDelivery.arrTOCEvents)
        {
            var iItem = GetCurrentItem(m_pDelivery.arrTOCEvents, fVidPos);
            m_pThumbnails.SelectThumbByIndex(iItem);
        }

        // send the current state to the tabcontrol
        var pStatus =
        {      
            Time: fVidPos,
            PlayState: m_pVideo ? m_pVideo.GetPlayState() : "Stopped"
        };

        if (m_pTabViewer)
        {
            m_pTabViewer.UpdateStatus(pStatus, bUserInitiated);
        }

        if (m_pEventTabViewer)
        {
            m_pEventTabViewer.UpdateStatus(pStatus);
        }

        this.UpdateScormProgress(fVidPos);
    }
    this.OnVideoPositionChanged = OnVideoPositionChanged;

    var m_isBuffering = false;

    function StartBuffering()
    {
        // when buffering, pause our object video and keep track of our state
        if (!m_isBuffering && (null != m_pObjectVideo))
        {
            m_pObjectVideo.SetPlayState("Paused");
            m_isBuffering = true;
        }
    }

    function EndBuffering()
    {
        // when done start playing the object video again
        if (m_isBuffering && (null != m_pObjectVideo))
        {
            m_isBuffering = false;
            m_pObjectVideo.SetPlayState("Playing");
        }
    }
    
    // called when our archival video changes play state
    function OnPlayStateChanged(playState)
    {
        if (playState == "Buffering")
        {
            // if we are buffering, pause the object video
            StartBuffering();
        }
        else if (m_isBuffering)
        {
            // we are not now buffering but were buffering, so start playing the object video again  
            EndBuffering();
        }
        else
        {
            if (m_pObjectVideo != null)
            {
                // we were not buffering and are now not buffering, so synchronize the object video's play state
                m_pObjectVideo.SetPlayState(playState);
            }
        }
    }

    this.OnObjectVideoPlayStateChanged = function(playState)
    {
        // this handles the case where object video gets initialized after archival video is playing (clicking a tab)
        // it simply synchronizes play states when the object video is not buffering
        if ((playState != "Buffering") && m_pVideo && (playState != m_pVideo.GetPlayState()))
        {
            m_pObjectVideo.SetPlayState(m_pVideo.GetPlayState());
        }
    }

    // needed to allow tabviewer to pass in our object video instance
    this.SetObjectVideoPlayer = function(player)
    {
        m_pObjectVideo = player;
    }

    this.SetVideoPosition = function (fVidPos) 
    {
        if (m_pVideo) 
        {
            m_pVideo.SetVideoPosition(fVidPos);
        }
        this.OnVideoPositionChanged(fVidPos, true);
    }

    this.SelectObjectView = function(viewID)
    {
        m_pTabViewer.SelectView(viewID);
    }
    
    // callback to synchronize streams
    function Synchronize()
    {
        // take the later of the actual and lastsetvideo positions

        // initialize fVidPos to the last position, in case m_pVideo is null.
        var fVidPos = m_pVideo ? m_pVideo.GetVideoPosition() : m_fLastSetVideoPosition; 

        // case where actual time is before the last set time
        if (fVidPos < m_fLastSetVideoPosition)
        {
            fVidPos = m_fLastSetVideoPosition;
        }

        this.OnVideoPositionChanged(fVidPos, false);
    }

    
    // event handler for window resize
    function OnResize()
    {
        if (m_el.style.display == "none") return;
        
        // if window is resizing while we're waiting for DeliveryInfo, early out since m_pVideo will be null.
        if (!m_pVideo) return;

        // determine our viewer height and set our outermost div - subtract 10 for the margin
        var viewerHeight = GetWindowHeight() - m_el.offsetTop - g_dContainerSpacing;
        viewerHeight = Math.max(g_dMinViewerHeight, viewerHeight);

        m_el.style.height = viewerHeight + "px";

        var viewerWidth = GetWindowWidth();
        viewerWidth = Math.max(g_dMinViewerWidth, viewerWidth);

        // set the height of our panes
        var leftPane = document.getElementById("leftPane");
        leftPane.style.height = viewerHeight + "px";

        var rightPane = document.getElementById("rightPane");
        rightPane.style.height = viewerHeight + "px";

        // call SetHeight for our events viewer and tab viewer
        // tab viewer is adjusted based on thumbnail display
        var playbackRateHeight = document.getElementById("playbackRateDiv").clientHeight;
        if (m_bHasObjectRegion)
        {
            var leftPaneWidth = m_bObjectRegionMaximized
                ? g_dMinEventViewerWidth
                : Math.max(g_dMinEventViewerWidth, viewerWidth * g_fLeftPaneWidthPerc);

            var videoWidthAvailable = leftPaneWidth - (g_dContainerSpacing * 2);
            var videoHeightAvailable = viewerHeight - g_dMinEventViewerHeight - playbackRateHeight;
            m_pVideo.OnResize(videoHeightAvailable, videoWidthAvailable);

            // Video may have shrunk to accommodate min event viewer height
            leftPaneWidth = Math.max(g_dMinEventViewerWidth, m_pVideo.Width + (g_dContainerSpacing * 2));
            leftPane.style.width = leftPaneWidth + "px";
            rightPane.style.left = leftPaneWidth + "px";
            rightPane.style.width = viewerWidth - leftPaneWidth + "px";

            var eventTabViewerHeight = viewerHeight - m_pVideo.Height - playbackRateHeight;
            eventTabViewerHeight = Math.max(g_dMinEventViewerHeight, eventTabViewerHeight);

            m_pEventTabViewer.SetHeight(eventTabViewerHeight);

            var tabViewerHeight = viewerHeight;
            if (m_bHasObjectRegion && !m_bObjectRegionMaximized && m_bShowThumbnails)
            {
                tabViewerHeight -= g_dThumbnailHeight;

                m_pThumbnails.SetVisible(true);
                m_pThumbnails.SetWidth(rightPane.offsetWidth - g_dContainerSpacing - (g_dBorderWidth * 2));
            }
            else
            {
                m_pThumbnails.SetVisible(false);
            }

            // Display questions tab for live broadcasts.
            if (g_pViewer.isLive)
            {
                tabViewerHeight -= (g_dQuestionsHeight + g_dContainerSpacing);
                m_pQuestions.OnResize(rightPane.offsetWidth - g_dContainerSpacing - (g_dBorderWidth * 2));
            }

            if (!m_pTabViewer.ZoomClicked)
            {
                if (viewerHeight < g_dMinObjectWindowHeight)
                {
                    m_pTabViewer.Zoom(true);
                }
                else
                {
                    m_pTabViewer.Zoom(false);
                }
            }

            var tabViewerWidth = rightPane.offsetWidth - g_dContainerSpacing;
            m_pTabViewer.OnResize(tabViewerHeight, tabViewerWidth);
        }
        else
        {
            m_pVideo.OnResize(viewerHeight - playbackRateHeight, viewerWidth - g_dMinEventViewerWidth - g_dContainerSpacing * 3);
            m_pEventTabViewer.SetWidth(viewerWidth - m_pVideo.Width - g_dContainerSpacing * 3);
            m_pEventTabViewer.SetHeight(m_pVideo.Height + playbackRateHeight + g_dContainerSpacing);
            var videoControlsDiv = document.getElementById("videoWithControlsDiv");
            videoControlsDiv.style.position = "absolute";
            videoControlsDiv.style.width = m_pVideo.Width + g_dContainerSpacing * 2;
        }
    }
    this.Resize = OnResize;

    this.GetEventTabViewer = function ()
    {
        return m_pEventTabViewer;
    }

    this.HighlightThumbnail = function (fTime, bHighlighed)
    {
        if (m_pThumbnails)
        {
            var iItem = GetCurrentItem(m_pDelivery.arrTOCEvents, fTime);
            m_pThumbnails.HighlightThumbnailByIndex(iItem, bHighlighed);
        }
    }
}
