//  HULU CONFIDENTIAL MATERIAL. DO NOT DISTRIBUTE.
//  Copyright (C) 2009-2010 Hulu, LLC
//  All Rights Reserved
/**
 * 
 * htvPlayer.js
 * 
 * This class defines the common routines for the Player portion of HTV.
 * 
 *  It is organized as a module but masquerades as a view to htvController.
 *  Responsibilities:  update on screen overlay on top of video, and 
 *  listen to user input.   Upon a playvideo request, perform csel, etc and
 *  send a playlist to the platform for it to handle the details.
 */
/*jslint maxerr: 1000, nomen: false, evil: false, immed: true, plusplus: false */
/*global describe, LOG */
/*global _handleEvent, _hide, _stop, _playbackFinished, _pause, _resume */
/*global _loadEndpoints, onPlaylistReceived, onProgressivePlaylistRefresh */
/*global onPlaylistRequestError, onPlaylistRequestErrorProcess, _hideBufferingUI */
/*global _beginPlayback, _playerCanvasCallback, _resetPlaybackState, */
/*global _initializePlayerUI, _showBufferingUI, _seekTo, _playbackError */
/*global onVideoMetadataReceived, onNextVideoIDReceived, onNextVideoMetadataReceived, */
/*global _onEndpointsReceived, _enableAdBreaks, _getCumulativeTimeForContentTime  */
/*global _isActive, _fetchAdBreakFrom, _updatePlaybackProgress */
/*global _playFile, _resize, _enforcePlayerSize */

var $htv;

$htv.Player = (function () {
    /*jslint onevar: false */
    var STATE_MAXIMIZED = 0;
    var STATE_MINIMIZED = 1;
    var STATE_END_CREDITS = 2;
    
    // todo: these numbers are so magical.
    var BUG_EDGE_OFFSET = 23;
    var BUG_WIDTH_HEIGHT =  77;
    
    var _loadingImage, _canvasHandle, _loadingImageEnabled, _handleTimeProgress, _handlePlaybackEnded, _handleStateChanged, _bufferingTimer, _keyTimer, _playerTimer, _pendingContentID, _startOptions, _playerState, _adData, _bitrates, _bufferState, _videoMetadata, _nextVideoMetadata, _playlist, _adRequestInfo, _dynamicBugBL, _dynamicBugBR, _countdownBackground, _countdownText, _activeNode, _adBreakCumulativeTimes, _lastPodWatched, _seeking, _prerollsLeftToPlay, _seekDestination, _activeSeekInformation, _suppressSeekVideoProgress, _hasShownBufferingDialog, _loadingIndicator;
    /*jslint onevar: true */
    _bufferingTimer = $htv.Platform.createTimer(0, null, null);
    _keyTimer = $htv.Platform.createTimer(0, null, null);
    _playerTimer = $htv.Platform.createTimer(0, null, null);
    _canvasHandle = null;
    _loadingImage = null;
    _loadingIndicator = null;
    _loadingImageEnabled = false;
    _videoMetadata = null;
    _nextVideoMetadata = null;
    _dynamicBugBL = null;
    _dynamicBugBR = null;
    _countdownBackground = null;
    _countdownText = null;
    _lastPodWatched = -1000; // -1 actually represents a the postroll pod
    _seeking = false;
    _suppressSeekVideoProgress = false;
    _seekDestination = -1;
    _activeSeekInformation = null;
    _prerollsLeftToPlay = [];
    _hasShownBufferingDialog = false;
    
    _playerState = {
        seekMode: false,
        seekSpeed: 0,
        resuming: false,
        playlistRetries: 3,
        currentState: "INIT",
        nextState: null,
        videoProgress: 0,
        totalProgress: 0,
        completionRecorded: false,
        currentBitrate: 3200,
        currentSeekTime: 0,
        currentAdBreak: null,
        currentAdBreakIndex: 0,
        aspectRatio: "16x9",
        size: STATE_MINIMIZED,
        position: {},
        paused: false,
        endCreditsHit: false,
        endCreditsDismissed: false,
        lastVideoProgress: 0,
        accumulatedPlayback: 0,
        waitingToSwitch: false,
        pendingBitrate: 0
    };
    
    _videoMetadata = {
        videoURL: null,
        content_id: 0,
        duration: 0
    };
    
    _nextVideoMetadata = {
        content_id: 0
    };
    
    _bufferState = {
        lastBufferStartTime: -1
    };
    
    _playlist = {};
    _adBreakCumulativeTimes = [];
    _startOptions = {
        start_at_beginning: false,
        next_video_type: "show"
    };
    
    _pendingContentID = -1;
    
    //Enable to allow debug printing to the player label (visible on lower end models)
    
    
    // playlist service load (or reload, checks for breakhash.)
    function _loadPlaylist(contentid, bitrate) {
        var playlistURL, callback, body, adState, device, version;

        // Mark content id as pending in case playlist load fails or key is expired
        _pendingContentID = contentid;
        
        if ($htv.Constants.Endpoints.expired === true) {
            _loadEndpoints();
        } else {
            callback = onPlaylistReceived;
            if (bitrate !== undefined) {
                _playerState.currentBitrate = bitrate;
            }
            
            playlistURL = $htv.Constants.Endpoints.csel +
                "/playlist?video_id=" +
                contentid +
                ($htv.Profile.isLoggedIn() ? "&token=" + $htv.Profile.getUserToken() : "");
            // for emulator debugging (only works on QA playlist service)
            // playlistURL += "&local=1";
            
            device = $htv.Platform.properties.platformCode;
            if (device === undefined) {
                
                device = 2;
            }
            version = $htv.Platform.properties.platformVersion;
            if (version === undefined) {
                
                version = 1;
            }
            
            // TODO: need to pass this the same way as other banya calls to csel
            playlistURL += "&device=" + device +
                "&version=" +
                version +
                "&device_id=" +
                $htv.Platform.properties.deviceid;
            
            // include ad state if it's been saved previously (saved in onPlaylistReceived)
            adState = $htv.Platform.readLocalData("player", "ad_state");
            body = "bitrate=" + bitrate +
                "&guid=" +
                $htv.Utils.urlEncode($htv.Beacons.getComputerGUID()) +
                "&kv=" +
                $htv.Constants.Endpoints.key_id +
                ($htv.Utils.stringIsNullOrEmpty(adState) ? "" : "&adstate=" + $htv.Utils.urlEncode(adState));
            if (_playlist.breakhash !== undefined) {
                body += "&breakhash=" + $htv.Utils.urlEncode(_playlist.breakhash);
                callback = onProgressivePlaylistRefresh;
            }
            
            $htv.Platform.loadURL(playlistURL, this, callback, {
                method: "POST",
                body: body,
                content_type: "application/x-www-form-urlencoded",
                xmlparse: false,
                errorCallback: onPlaylistRequestError,
                timeout: 3000,
                compress: $htv.Platform.properties.has_gzip
            });
            
        }
    }
    
    function onPlaylistRequestError(status, errorObj) {
        // process the error based on the response body
        onPlaylistRequestErrorProcess(errorObj.data);
    }
    
    function onPlaylistRequestErrorProcess(errorMsg, options) {
        
        var items, handled, allowAutoRetry, message, allowUserRetry, buttonYInitial, textXPadding, textWidth;
        handled = false; // error already handled?
        allowAutoRetry = false; // should auto-retry?
        message = "The selected video could not be played. Sorry for the inconvenience."; // dialog error message
        allowUserRetry = true; // include a retry button on dialog?
        buttonYInitial = 270; // y to start the buttons at
        textXPadding = 70;
        textWidth = 420;
        
        // Strip out leading '*' char
        if (errorMsg && errorMsg.charAt(0) === "*") {
            errorMsg = errorMsg.substr(1);
        }
        
        switch (errorMsg) {
        case "Under Age":
            message = "Sorry, the video you have requested is restricted to users " + options.min_age + " years and older. This Hulu Plus account restricts access to content rated for ages " + options.current_age + " and below.";
            buttonYInitial = 340;
            allowUserRetry = false;
            break;
        case "Under Age Device":
            message = "Sorry, the video you have requested is restricted to users " + options.min_age + " years and older. The device parental controls currently restrict access to content rated for ages " + options.current_age + " and below.";
            buttonYInitial = 340;
            allowUserRetry = false;
            break;
        case "Invalid server key": // shouldn't happen unless server key expiration logic is wrong. call /config again.
            if (_playerState.playlistRetries > 0) {
                handled = true;
                _playerState.playlistRetries--;
                _loadEndpoints(true); // on success, pendingContentId should case playlist to be loaded again
            }
            break;
        case "Video expired":
            message = "The video you're trying to watch is no longer available.";
            allowUserRetry = false;
            $htv.Beacons.trackDataloadDeejay("expired");
            break;
        case "User banned":
            message = "Your account has been flagged by our support team and is currently inactive. Please contact our support team at 1-877-HULU-411 (1-877-485-8411), 6 am to 11 pm Pacific Time for further assistance.";
            allowUserRetry = false;
            buttonYInitial = 330;
            textXPadding = 40;
            textWidth = 480;
            break;
        case "User abuse":
            message = "Your Hulu Plus subscription allows you to watch one video at a time. To continue watching here, please close any Hulu Plus videos you may be watching on other devices. To manage your active devices, please visit hulu.com/devices.";
            allowUserRetry = false;
            buttonYInitial = 350;
            textXPadding = 40;
            textWidth = 480;
            break;
        case "Geo restriction":
            message = "We're sorry, but Hulu is a U.S.-only service and is not available outside the U.S. If you think you're receiving this message in error, please contact our customer support team at 1-877-HULU-411 (1-877-485-8411), 6 am to 11 pm Pacific Time.";
            allowUserRetry = false;
            buttonYInitial = 350;
            textXPadding = 40;
            textWidth = 480;
            break;
        case "Device not found":
        case "Invalid version":
        case "Video not found": // video doesn't exist or there is no streamable transcodes. This could happen if twinkie data is out of sync.
        case "User session expired":
            allowAutoRetry = true;
            break;
        default:
            allowAutoRetry = true;
            break;
        }
        
        if (!handled) {
            
            $htv.Beacons.trackDataloadDeejay("error");
            
            if (allowAutoRetry && _playerState.playlistRetries > 0) {
                
                _playerState.playlistRetries--;
                _loadPlaylist(_pendingContentID, _playerState.currentBitrate);
                return;
            }
            
            items = [{
                text: "OK",
                callback: function () {
                },
                receiver: this
            }];
            if (allowUserRetry) {
                items.push({
                    text: "Retry",
                    callback: function () {
                        $htv.Controller.playVideo(_pendingContentID, _startOptions);
                    },
                    receiver: this
                });
            }
            
            // Hide player and show dialog 
            _hideBufferingUI();
            _playbackFinished({force_stop: true});
            $htv.Controller.pushView("DialogBoxView", {
                text: message,
                items: items,
                textXPadding: textXPadding,
                textWidth: textWidth,
                buttonYInitial: buttonYInitial
            });
        }
    }
    
    function onPlaylistReceived(responseText, options) {
        var i, j, result, lastSeenAdBreakTime = -1, cumulativeAdTime = 0, lastSeenPos = -1000, lastSeenPod = -1000, slotsCount = 0, key, viewed, duration;
        
        
        // Mark this content id playlist as loaded
        _pendingContentID = -1;
        
        if (_playerState.currentState === "INIT") {
            
            return;
        }
        // If the 200 starts with '*', use the response as an error
        if (responseText.charAt(0) === "*") {
            
            onPlaylistRequestErrorProcess(responseText);
            return;
        }
        
        if ($htv.Constants.Endpoints.expired === true) {
            _loadEndpoints();
        } else { // decrypt
            try {
                result = $htv.Utils.parseDeejayResponse({
                    cipher_text: responseText,
                    device_key: $htv.Platform.properties.platformKey,
                    server_key: $htv.Constants.Endpoints.key
                });
            } 
            catch (e) {
                
                onPlaylistRequestErrorProcess(responseText);
                return;
            }
            
            describe(result);
            // could be an error: need to reload endpoints
            
            _playlist = result;
            _playerState.currentState = "PLAYLIST_RECEIVED";
            
            if (_playlist.min_age !== null && _playlist.min_age !== undefined) {
                
                if (_playlist.min_age > $htv.Profile.getAge()) {
                    onPlaylistRequestErrorProcess("Under Age", {
                        min_age: _playlist.min_age,
                        current_age: $htv.Profile.getAge()
                    });
                    return;
                } else if ($htv.Platform.properties.user && 
                        $htv.Platform.properties.user.age &&
                        _playlist.min_age > $htv.Platform.properties.user.age) {
                    onPlaylistRequestErrorProcess("Under Age Device", {
                        min_age: _playlist.min_age,
                        current_age: $htv.Platform.properties.user.age
                    });
                    return;
                }
            }
            
            // persist adstate
            if (!$htv.Utils.stringIsNullOrEmpty(_playlist.adstate)) {
                $htv.Platform.writeLocalData("player", "ad_state", _playlist.adstate);
            }
            
            // enable all adbreaks, and while we're at it process playlist to figure out 
            // what cumulative time chunks are content, ad
            _adBreakCumulativeTimes = [];
            
            // Setup the group slots (those that share the same pos attribute, even across pods)
            for (i = 0; i < _playlist.breaks.length; i++) {
                //  TODO: test this function.
                // prevent final tune-in from being lumped into the previous slot.
                // Consider: will this not work properly on the last iteration? There's another instance below.
                if ((_playlist.breaks[i].pod !== undefined || lastSeenPos !== -1000) && lastSeenPos !== _playlist.breaks[i].pos) {
                    lastSeenPos = _playlist.breaks[i].pos;
                    slotsCount = 0;
                }
                _playlist.breaks[i].group_slot = slotsCount;
                slotsCount++;
                // apply the new slots count to old slots
                for (j = i - slotsCount + 1; j <= i; j++) {
                    _playlist.breaks[j].group_slot_count = slotsCount;
                }
            }
            
            slotsCount = 0;
            // todo: refactor (complicated)
            for (i = 0; i < _playlist.breaks.length; i++) {
                _playlist.breaks[i].enabled = true;
                _playlist.breaks[i].breaks_index = i; // can be used to determine the breaks[i] index from an ad object
                // process cumulative times
                if (_adBreakCumulativeTimes.length === 0 || lastSeenAdBreakTime !== _playlist.breaks[i].pos) {
                    _adBreakCumulativeTimes.push([cumulativeAdTime + _playlist.breaks[i].pos, cumulativeAdTime + _playlist.breaks[i].pos + _playlist.breaks[i].duration]);
                } else {
                    _adBreakCumulativeTimes[_adBreakCumulativeTimes.length - 1][1] += _playlist.breaks[i].duration;
                }
                
                if (_playlist.breaks[i].pod === undefined) { // Add all the prerolls to an array.
                    _prerollsLeftToPlay.push(_playlist.breaks[i]);
                }
                
                // process pods/slots
                if (lastSeenPod !== _playlist.breaks[i].pod) {
                    lastSeenPod = _playlist.breaks[i].pod;
                    slotsCount = 0;
                }
                _playlist.breaks[i].slot = slotsCount;
                _playlist.breaks[i].cum_pos = cumulativeAdTime + _playlist.breaks[i].pos;
                
                slotsCount++;
                for (j = i - slotsCount + 1; j <= i; j++) {
                    _playlist.breaks[j].slot_count = slotsCount;
                }
                
                // process beacon params
                _playlist.breaks[i].collected_audit_params = {};
                if (_playlist.adaudit_params) {
                    for (key in _playlist.adaudit_params) {
                        if (_playlist.adaudit_params.hasOwnProperty(key)) {
                            _playlist.breaks[i].collected_audit_params[key] = _playlist.adaudit_params[key];
                        }
                    }
                }
                if (_playlist.breaks[i].audit_params) {
                    for (key in _playlist.breaks[i].audit_params) {
                        if (_playlist.breaks[i].audit_params.hasOwnProperty(key)) {
                            _playlist.breaks[i].collected_audit_params[key] = _playlist.breaks[i].audit_params[key];
                        }
                    }
                }
                if (_playlist.breaks[i].video_audit_params) {
                    for (key in _playlist.breaks[i].video_audit_params) {
                        if (_playlist.breaks[i].video_audit_params.hasOwnProperty(key)) {
                            _playlist.breaks[i].collected_audit_params[key] = _playlist.breaks[i].video_audit_params[key];
                        }
                    }
                }
                
                lastSeenAdBreakTime = _playlist.breaks[i].pos;
                cumulativeAdTime += _playlist.breaks[i].duration;
            }
            
            
            describe(_adBreakCumulativeTimes);
            
            // for non-auto livestreaming, choose new bitrate locally
            if ($htv.Platform.properties.livestreaming === true && $htv.Platform.properties.livestreaming_autoswitch !== true) {
                // ensure descending
                _playlist.bitrates.sort(function (a, b) {
                    return b - a;
                });
                // select the starting bitrate
                for (i = _playlist.bitrates.length - 1; i >= 0; i--) {
                    // if previous bitrate isn't available, choose next lowest bitrate
                    if (_playlist.bitrates[i] < _playerState.currentBitrate &&
                    (i - 1 < 0 || _playlist.bitrates[i - 1] > _playerState.currentBitrate)) {
                        _playerState.currentBitrate = _playlist.bitrates[i];
                        break;
                    }                    // choose previous bitrate, or next highest if lower isn't available
                    else if (_playlist.bitrates[i] >= _playerState.currentBitrate) {
                        _playerState.currentBitrate = _playlist.bitrates[i];
                        break;
                    }
                }
            } // For progressive, use the playlist response as the authority
            else if ($htv.Platform.properties.livestreaming !== true) {
                _playerState.currentBitrate = _playlist.stream_bitrate;
            }
            
            _playerState.videoProgress = 0;
            if (_startOptions.start_at_beginning !== true) {
                viewed = _playlist.pb_position;
                duration = _playlist.duration;
                if (viewed && viewed > 0) {
                    
                    _playerState.videoProgress = viewed;
                    _playerState.resuming = true;
                    if ($htv.Platform.properties.livestreaming === true) {
                        _seekDestination = _getCumulativeTimeForContentTime(viewed);
                    } else {
                        _seekDestination = viewed;
                    }
                } else {
                    
                    _playerState.videoProgress = 0;
                    _seekDestination = -1;
                }
            }
            
            $htv.Controller.fireEvent("PLAYER_PLAYLIST_RECEIVED", {
                playlist: _playlist,
                currentBitrate: _playerState.currentBitrate,
                resumePosition: _playerState.videoProgress
            });
            _beginPlayback();
            
            // make sure position has been set at least once
            if (!_playerState.position.hasOwnProperty("height")) {
                _enforcePlayerSize();
            }
            if (!$htv.Utils.stringIsNullOrEmpty(result.dynamic_bug_bl)) {
                _dynamicBugBL = $htv.Platform.createImage({
                    x: _playerState.position.x + (_playlist.aspect_ratio === "4x3" ? (120 * (_playerState.position.height / 540)) : 0) + (BUG_EDGE_OFFSET * (_playerState.position.height / 540)),
                    y: _playerState.position.y + _playerState.position.height - ((BUG_WIDTH_HEIGHT + BUG_EDGE_OFFSET) * (_playerState.position.height / 540)),
                    z: 11,
                    width: BUG_WIDTH_HEIGHT * (_playerState.position.height / 540),
                    height: BUG_WIDTH_HEIGHT * (_playerState.position.height / 540)
                }, result.dynamic_bug_bl, _canvasHandle);
                _dynamicBugBL.hide();
            }
            if (!$htv.Utils.stringIsNullOrEmpty(result.dynamic_bug_br)) {
                _dynamicBugBR = $htv.Platform.createImage({
                    x: _playerState.position.x + _playerState.position.width - (_playlist.aspect_ratio === "4x3" ? (120 * (_playerState.position.height / 540)) : 0) - ((BUG_WIDTH_HEIGHT + BUG_EDGE_OFFSET) * (_playerState.position.height / 540)),
                    y: _playerState.position.y + _playerState.position.height - ((BUG_WIDTH_HEIGHT + BUG_EDGE_OFFSET) * (_playerState.position.height / 540)),
                    z: 11,
                    width: BUG_WIDTH_HEIGHT * (_playerState.position.height / 540),
                    height: BUG_WIDTH_HEIGHT * (_playerState.position.height / 540)
                }, result.dynamic_bug_br, _canvasHandle);
                _dynamicBugBR.hide();
            }
            
        }
    }
    
    function _loadEndpoints(forceExpiration) {
        $htv.Controller.loadEndPoints(this, _onEndpointsReceived, forceExpiration);
    }
    
    function _onEndpointsReceived(result) {
        var loadCID;
        
        if (_pendingContentID !== -1) {
            loadCID = _pendingContentID;
            _pendingContentID = -1;
            _loadPlaylist(loadCID, _playerState.currentBitrate);
        }
    }
    
    
    
    function _playVideo(contentid, options) {
        
        $htv.Controller.fireEvent("PLAYER_PLAYBACK_REQUESTED", {});
        $htv.Platform.initializePlayer(this, _playerCanvasCallback);
        $htv.Platform.stopVideo();
        // fire playback banya call (type 1) using playlist view_token 
        // if there's no view_token, it's not plus_only content, so we don't need to fire. 
        if (_playerState.currentState !== "INIT" && _playlist.view_token) {
            $htv.Controller.makeBanyaRequest(1, _playlist.view_token);
        }
        _resetPlaybackState();
        _playerState.currentState = "LOADING";
        _initializePlayerUI();
        if (options && options.hasOwnProperty("next_video_size")) {
            _resize(options.next_video_size, true);
        }
        else {
            _resize(STATE_MAXIMIZED, true);
        }
        _showBufferingUI(0);
        _videoMetadata.content_id = contentid;
        if (options && options.hasOwnProperty("dismiss_end_credits")) {
            _playerState.endCreditsDismissed = options.dismiss_end_credits;
        }
        if (options && options.hasOwnProperty("next_video_type")) {
            _startOptions.next_video_type = options.next_video_type;
            
        }
        if (options && options.hasOwnProperty("start_at_beginning")) {
            _startOptions.start_at_beginning = options.start_at_beginning;
        }
        if (options && options.hasOwnProperty("disable_autoplay")) {
            _startOptions.disable_autoplay = options.disable_autoplay;
        }
        _startOptions.bitrate = $htv.Profile.getPreference("default_bitrate");
        if ($htv.Utils.stringIsNullOrEmpty(_startOptions.bitrate)) {
            _startOptions.bitrate = $htv.Platform.getDefaultBitrate();
        }
        if (options && options.hasOwnProperty("bitrate")) {
            _startOptions.bitrate = options.bitrate;
        }
        _startOptions.bitrate = parseInt(_startOptions.bitrate, 10);
        _loadPlaylist(contentid, _startOptions.bitrate);
        $htv.Utils.makeMetadataRequestWithContentID(contentid, this, onVideoMetadataReceived, options.package_id);
    }
    
    function onVideoMetadataReceived(videoMetadata) {
        
        for (var key in videoMetadata) {
            if (videoMetadata.hasOwnProperty(key)) {
                _videoMetadata[key] = videoMetadata[key];
            }
        }
        
        _videoMetadata.end_credits_time = $htv.Utils.getEndCreditsTimeFromString(_videoMetadata.end_credits_time);
        $htv.Controller.fireEvent("PLAYER_METADATA_RECEIVED", {
            metadata: _videoMetadata
        });
        
        // todo: not the best place, but we are playing using content_id (and these calls need video id and pid)
        $htv.Profile.recordVideoViewStart(_videoMetadata.video_id);
        
        //autoplay info request
        $htv.Profile.getNextVideo(_videoMetadata.content_id, _startOptions.next_video_type, this, onNextVideoIDReceived);
        
    }
    
    function onNextVideoIDReceived(result) {        
        if (result === null || result.error !== "" ||
        result.video_id === undefined || result.video_id === null || result.video_id === 0) {
            
            _nextVideoMetadata = { content_id: 0 };
            return;
        }
        
        $htv.Utils.makeMetadataRequestWithVideoID(result.video_id, this, onNextVideoMetadataReceived);
    }
    
    function onNextVideoMetadataReceived(nextMetadata) {
        
        for (var key in nextMetadata) {
            if (nextMetadata.hasOwnProperty(key)) {
                _nextVideoMetadata[key] = nextMetadata[key];
            }
        }
    }
    
    // currently, only the label exists.
    function _initializePlayerUI() {
        if (_loadingImage === null) {
            _loadingImage = $htv.Platform.createImage({
                x: 0,
                y: 0,
                z: 8,
                width: 960,
                height: 540
            }, "images/loading-back.png", _canvasHandle);
        }
        _loadingImage.hide();
        _loadingImageEnabled = false;
        
        if (_loadingIndicator === null) {
            _loadingIndicator = $htv.ControlPool.getObject("LoadingIndicator");
            _loadingIndicator.initialize({
                x: 410,
                y: 290,
                z: 9,
                width: 960,
                height: 540
            }, null, null, {});
            
        }
        _loadingIndicator.hide();
        
        if (_countdownBackground === null) {
            _countdownBackground = $htv.Platform.createBox({
                x: 0,
                y: 0,
                z: 11,
                width: 960,
                height: 31
            }, "CountdownBackground", _canvasHandle);
        }
        _countdownBackground.hide();
        
        if (_countdownText === null) {
            _countdownText = $htv.Platform.createLabel({
                x: 15,
                y: 5,
                z: 12,
                width: 600,
                height: 20
            }, "", _canvasHandle, {
                styleName: $htv.Styles.CountdownText
            });
        } else {
            _countdownText.setText("");
        }
        _countdownText.hide();
        
        // clean these out, will be created on playlist received
        if (_dynamicBugBL !== null) {
            $htv.Platform.deleteItem(_dynamicBugBL);
            _dynamicBugBL = null;
        }
        if (_dynamicBugBR !== null) {
            $htv.Platform.deleteItem(_dynamicBugBR);
            _dynamicBugBR = null;
        }
        
        // Reset the progress UI to 0
        $htv.Controller.fireEvent("PLAYER_TIME_PROGRESS", {
            milliseconds: 0
        });
    }
    
    function _resetPlaybackState() {
        
        
        _playerState = {
            seekMode: false,
            seekSpeed: 0,
            resuming: false,
            playlistRetries: 3,
            currentState: "INIT",
            nextState: null,
            videoProgress: 0,
            totalProgress: 0,
            completionRecorded: false,
            currentBitrate: 3200,
            currentSeekTime: 0,
            currentAdBreak: null,
            currentAdBreakIndex: 0,
            aspectRatio: "16x9",
            size: STATE_MINIMIZED,
            position: {},
            paused: false,
            endCreditsHit: false,
            endCreditsDismissed: false,
            lastVideoProgress: 0,
            accumulatedPlayback: 0,
            waitingToSwitch: false,
            pendingBitrate: 0
        };
        
        _bufferState = {
            lastBufferStartTime: -1
        };
        
        _startOptions = {
            bitrate: $htv.Profile.getPreference("default_bitrate"),
            start_at_beginning: false,
            next_video_type: "show"
        };
        
        _videoMetadata = {
            videoURL: null,
            content_id: 0,
            duration: 0
        };
        
        _nextVideoMetadata = {
            content_id: 0
        };
        
        _playlist = {};
        _seekDestination = -1;
        _prerollsLeftToPlay = [];
    }
    
    function _showBufferingUIDelayed() {
        
        _bufferingTimer.stop();
        if (_loadingImageEnabled === true) {
            _loadingImage.show();
            if (_playerState.size === STATE_MAXIMIZED) {
                _loadingIndicator.show();
            }
        }
    }
    
    function _showBufferingUI(wait) {
        wait = (wait === undefined) ? 500 : wait;
        _loadingImageEnabled = true;
        if (wait > 0) {
            
            _bufferingTimer.setCallback(this, _showBufferingUIDelayed);
            _bufferingTimer.setInterval(wait);
            _bufferingTimer.start();
        } else {
            _loadingImage.show();
            if (_playerState.size !== STATE_MINIMIZED) {
                _loadingIndicator.show();
            }
        }
    }
    
    function _hideBufferingUI() {
        
        _loadingImageEnabled = false;
        _bufferingTimer.stop();
        if (_loadingImage !== null) {
            _loadingImage.hide();
        }
        if (_loadingIndicator !== null) {
            _loadingIndicator.hide();
        }
    }
    
    function _decreaseBitrate() {
        // find largest bitrate smaller than current bitrate
        var i, targetBitrate = 0;
        for (i = 0; i < _playlist.bitrates.length; i++) {
            if (_playerState.currentBitrate > _playlist.bitrates[i]) {
                targetBitrate = _playlist.bitrates[i];
            } else {
                break;
            }
        }
        if (targetBitrate === 0) {
            return; // can't go any further down.
        }
        _loadPlaylist(_videoMetadata.content_id, targetBitrate);
    }
    
    function _increaseBitrate() {
        // find next largest bitrate
        var i, targetBitrate = _playerState.currentBitrate;
        for (i = 0; i < _playlist.bitrates.length; i++) {
            if (_playerState.currentBitrate < _playlist.bitrates[i]) {
                targetBitrate = _playlist.bitrates[i];
                break;
            }
        }
        if (targetBitrate === _playerState.currentBitrate) {
            return; // can't go any further up
        }
        _loadPlaylist(_videoMetadata.content_id, targetBitrate);
    }
    
    function onProgressivePlaylistRefresh(responseText) {
        var i, playlistRefresh;
        playlistRefresh = $htv.Utils.parseDeejayResponse({
            cipher_text: responseText,
            device_key: $htv.Platform.properties.platformKey,
            server_key: $htv.Constants.Endpoints.key
        });
        
        _playlist.stream_url = playlistRefresh.stream_url;
        for (i = 0; i < _playlist.breaks.length; i++) {
            _playlist.breaks[i].url = playlistRefresh.breaks[i].url;
        }
        _playerState.currentState = "SWITCHING_BITRATE";
        _playFile(_playlist.stream_url, _playerState.videoProgress, _playlist.aspect_ratio, _playerState.currentBitrate);
    }
    
    function _playAdBreak(adBreakObj) {
        _playerState.currentAdBreak = adBreakObj;
        _playerState.currentState = "BREAK";
        _playerState.currentAdBreak.last_beaconed_pos = 0;
        _playerState.currentAdBreak.last_audited_pos = -1;
        
        adBreakObj.enabled = false;
        
        if (adBreakObj.slot === adBreakObj.slot_count - 1) { // Check if last ad of the pod.
            _lastPodWatched = adBreakObj.pod;
        }
        
        // b: send before each ad break.
        $htv.Beacons.trackPlaybackPosition(true);
        _updatePlaybackProgress(true);
        
        // Xinan says: if deejay doesn't specify ad's aspect ratio, default to content's
        _playFile(adBreakObj.url, 0, (adBreakObj.hasOwnProperty("aspect_ratio") ? adBreakObj.aspect_ratio : _playlist.aspect_ratio));
        _showBufferingUI();
        
        // beacon revenue/start
        if (adBreakObj.pod !== undefined) {
            $htv.Beacons.trackRevenueStart(adBreakObj);
        }
    }
        
    function _beginPlayback() {
        var viewed, duration, nextBreak, adbreak;
        _playerState.currentState = "STARTING";
        _playerState.lastVideoProgress = _playerState.videoProgress;
        _playerState.accumulatedPlayback = 0;
        _enableAdBreaks();
        
        
        nextBreak = _fetchAdBreakFrom(0); // This parameter is irrelevant as we'll get prerolls regardless since we're just beginning playback.
        if ($htv.Platform.properties.livestreaming === true) {
            _playerState.currentState = "STARTING_VIDEO";
            _playFile(_playlist.stream_url, _playerState.videoProgress, _playlist.aspect_ratio, _playerState.currentBitrate);
            _showBufferingUI();
        } else {
            if (nextBreak !== null && Math.floor(nextBreak.pos * 1000) <= _playerState.videoProgress) {
                _playAdBreak(nextBreak);
            } else {
                _playerState.currentState = "STARTING_VIDEO";
                _playFile(_playlist.stream_url, _playerState.videoProgress, _playlist.aspect_ratio, _playerState.currentBitrate);
                _showBufferingUI();
            }
        }
        $htv.Controller.fireEvent("PLAYER_PLAYING", {});
        _updatePlaybackProgress(true);
        _playerTimer.setCallback(this, _updatePlaybackProgress);
        _playerTimer.setInterval(3000); // 3 second updates.
        _playerTimer.start();
    }
    
    function _fetchAdBreakInRange(from, to) {
        var i, curBreak, fetchedIndex = -1;
        for (i = 0; _playlist.breaks && i < _playlist.breaks.length; i++) {
            curBreak = _playlist.breaks[i];
            // If break is enabled and is in requested range
            if (curBreak && curBreak.enabled === true &&
            ((curBreak.pos >= from && curBreak.pos <= to) || (curBreak.pos >= to && curBreak.pos <= from))) {
            
                // If seeking back, take the first one
                if (from > to) {
                    fetchedIndex = i;
                    break;
                } else if (fetchedIndex === -1 || _playlist.breaks[fetchedIndex].pos < curBreak.pos) {
                    // If going forward, always choose the first break with identical pos (2 ads per pod)
                    fetchedIndex = i;
                }
            }
        }
        
        if (fetchedIndex !== -1) {
            _playerState.currentAdBreakIndex = fetchedIndex;
            
            // If the ad we're scheduled to watch next was the last ad we watched, we'll return null, indicating that we want to skip it.
            return (_playlist.breaks[fetchedIndex].pod !== _lastPodWatched) ? _playlist.breaks[fetchedIndex] : null;
        }
        return null;
    }
    
    /*
     * Returns the first preroll that hasn't been played if one exists, else
     * returns the next ad starting from currentPosition.
     */
    function _fetchAdBreakFrom(currentPosition) {
        var i, adBreak, tempAdBreak;
        if (_prerollsLeftToPlay.length > 0) {
            return _prerollsLeftToPlay.shift();
        }
        
        adBreak = null;
        for (i = 0; i < _playlist.breaks.length; i++) {
            tempAdBreak = _playlist.breaks[i];
            if (tempAdBreak.pod !== undefined && // We take care of the prerolls above.
            tempAdBreak.enabled === true && // Only want an enabled ad.
            tempAdBreak.pos >= currentPosition) { // Next ad, chronologically.
                // If the ad we're scheduled to watch next was the last ad we watched, we'll return null, indicating that we want to skip it.
                return (tempAdBreak.pod !== _lastPodWatched) ? tempAdBreak : null;
            }
        }
        
        return adBreak;
    }
    
    /*
     * Returns true if there are more ads at the position specified that need to be played; false otherwise.
     */
    function _moreAdsAtPos(position) {
        for (var i = 0; i < _playlist.breaks.length; i++) {
            if (_playlist.breaks[i].pos === position && _playlist.breaks[i].enabled === true) {
                return true;
            }
            if (_playlist.breaks[i].pos > position) {
                return false;
            }
        }
        
        return false;
    }
    
    function _disableAdBreaks() {
        for (var i = 0; i < _playlist.breaks.length; i++) {
            _playlist.breaks[i].enabled = false;
        }
    }
    
    function _enableAdBreaks() {
        for (var i = 0; i < _playlist.breaks.length; i++) {
            _playlist.breaks[i].enabled = true;
        }
    }
    
    /*
     * Works only for non-prerolls.
     */
    function _getTimeLeftForAdsAt(position, groupSlotIndex) {
        var i, breakTime = 0;
        for (i = 0; i < _playlist.breaks.length; i++) {
            if (_playlist.breaks[i].pod !== undefined && _playlist.breaks[i].pos === position && _playlist.breaks[i].group_slot >= groupSlotIndex) {
                breakTime += _playlist.breaks[i].duration;
            }
        }
        return breakTime;
    }
    
    function _getCumulativeTimeForContentTime(milliseconds) {
        var i, cumulativeTime = 0;
        for (i = 0; i < _playlist.breaks.length; i++) {
            if (_playlist.breaks[i].pos <= milliseconds) {
                cumulativeTime += _playlist.breaks[i].duration;
            }
        }
        return cumulativeTime + milliseconds;
    }
    
    function _instantReplay() {
        var cumulativeTime, timeDiff, minTimeDiff = -1, i, length = _adBreakCumulativeTimes.length;
        
        cumulativeTime = _getCumulativeTimeForContentTime(_playerState.videoProgress);
        
        for (i = 0; i < length; i++) {
            timeDiff = cumulativeTime - _adBreakCumulativeTimes[i][1];
            if (minTimeDiff === -1 || (timeDiff > 0 && timeDiff < minTimeDiff)) {
                minTimeDiff = timeDiff;
            }
        }
        
        // check whether we're < 15 seconds from adbreak.
        if (minTimeDiff < 15000) {     
            
            _seekTo(_playerState.videoProgress - minTimeDiff);
        } else {
            if ($htv.Platform.properties.has_instant_replay_api === true) {
                $htv.Platform.instantReplay();
            } else {
                // just seek back 7 seconds.. or should we call Platform.seekby?
                _seekTo(_playerState.videoProgress - 7000);
            }
        }
    }
    
    
    // milliseconds = content milliseconds.
    function _seekTo(milliseconds) {
        var actualTime, adBreak, currentPosition;
        
        if (milliseconds < 0) {
            milliseconds = 0;
        }
        _seeking = true;
        adBreak = null;
        if (_playerState.lastAccumulatedPlayback >= $htv.Constants.AD_FREE_SEEK_PERIOD) {
            adBreak = _fetchAdBreakInRange(_playerState.videoProgress, milliseconds);
        }
        
        currentPosition = (_activeSeekInformation !== null) ? _activeSeekInformation.expected_destination : _playerState.videoProgress;
                
        _activeSeekInformation = {
            current_position: currentPosition,
            expected_destination: milliseconds
        };        
        
        $htv.Controller.fireEvent("PLAYER_SEEK_REQUESTED", _activeSeekInformation);
        
        if ($htv.Platform.properties.livestreaming === true) {
            _seekDestination = _getCumulativeTimeForContentTime(milliseconds);
        } else {
            _seekDestination = milliseconds;
        }
        
        // If there's an ad break, play ad break first
        if (adBreak !== null) {
            // Update content's progress and controls' UI
            _playerState.videoProgress = milliseconds;
            $htv.Controller.fireEvent("PLAYER_TIME_PROGRESS", {
                milliseconds: milliseconds
            });
            if ($htv.Platform.properties.livestreaming === true) {
                
                if (milliseconds === 0) {
                    actualTime = _getCumulativeTimeForContentTime(milliseconds);
                    
                    $htv.Platform.seekTo(0, actualTime);
                } else {
                    
                    _suppressSeekVideoProgress = true;
                    $htv.Platform.seekTo(0, adBreak.cum_pos);
                    $htv.Controller.fireEvent("PLAYER_SEEKING_TO_AD", {});
                }
            } else {
                _playAdBreak(adBreak);
            }
        } else {
            if ($htv.Platform.properties.livestreaming === true) {
                $htv.Platform.seekTo(0, _seekDestination);
                _seekDestination = -1; // don't need seek destination since not playing ad first
            } else {
                $htv.Platform.seekTo(0, milliseconds);
            }
            $htv.Controller.fireEvent("PLAYER_SEEKING", {});
        }
    }
    
    function _updatePlaybackProgress(forcePOST, callback) {
        if (_videoMetadata.content_id === 0) {
            
        }
        $htv.Profile.setPlaybackProgress(_videoMetadata.content_id, _playerState.videoProgress / 1000, _videoMetadata.duration, forcePOST, callback);
    }
    
    function _playerCanvasCallback(canvasHandle) {
        
        _canvasHandle = canvasHandle;
    }
    
    function _updateStateUI() {
        var showBugs, showCountdown, showLoadingScreen, greenAdBreakIndex, i, minDiff;
        
        
        
        // Make decisions about bugs and ad countdown
        showBugs = false;
        showCountdown = false;
        
        greenAdBreakIndex = -1;
        
        if (_playerState.currentState === "BREAK") {
            showBugs = false;
            describe(_playerState.currentAdBreak);
            if (_playerState.currentAdBreak && _playerState.currentAdBreak.hasOwnProperty("can_display_timeline")) {
                showCountdown = _playerState.currentAdBreak.can_display_timeline && (_playerState.size !== STATE_MINIMIZED);
            }
            $htv.Controller.fireEvent("PLAYER_ADBREAK_PLAYING", {
                adBreakPos: _playerState.currentAdBreak.pos
            });
        } else if (_playerState.currentState === "VIDEO") {
            showBugs = true;
            showCountdown = (_playerState.size === STATE_END_CREDITS);
            $htv.Controller.fireEvent("PLAYER_ADBREAK_PLAYING", {
                adBreakPos: 0
            });
        }
        
        // Now make UI changes
        if (showCountdown) {
            if (_countdownBackground !== null) {
                _countdownBackground.show();
            }
            if (_countdownText !== null) {
                _countdownText.setText("");
                _countdownText.show();
            }
        } else {
            if (_countdownBackground !== null) {
                _countdownBackground.hide();
            }
            if (_countdownText !== null) {
                _countdownText.hide();
            }
        }
        
        if (showBugs) {
            if (_dynamicBugBL !== null) {
                _dynamicBugBL.show();
            }
            if (_dynamicBugBR !== null) {
                _dynamicBugBR.show();
            }
        } else {
            if (_dynamicBugBL !== null) {
                _dynamicBugBL.hide();
            }
            if (_dynamicBugBR !== null) {
                _dynamicBugBR.hide();
            }
        }
    }
    function _findAdBreakByCumulativeTime(milliseconds) {
        // 
        var i, cumulativeAdTime = 0;
        for (i = 0; i < _playlist.breaks.length; i++) {
            if (milliseconds <= cumulativeAdTime + _playlist.breaks[i].pos + _playlist.breaks[i].duration && cumulativeAdTime + _playlist.breaks[i].pos <= milliseconds) {
                // 
                return _playlist.breaks[i];
            }
            cumulativeAdTime += _playlist.breaks[i].duration;
        }
        // 
        return null;
    }
    
    function _handleEvent(eventName, eventData) {
        if (eventName === "PROFILE_STATE_CHANGED") {
            if (!$htv.Profile.isLoggedIn() && _isActive()) {
                _stop({force_stop: true});
            }
        }
    }

    if ($htv.Platform.properties.livestreaming === true) {
        
        _handleTimeProgress = function (playerId, eventType, eventData) {
            // todo: determine where we are given the cumulative time in eventData.milliseconds
            var isVideoProgress = true, i = 0, lastBeaconedPos, slotProgress, currentAdBreak, cumulativeTimeOffset, timeProgress = 0, resumeSeconds, countdownText, cumulativeAdTimeIndex;
            for (i = 0; i < _adBreakCumulativeTimes.length; i++) {
                if (_adBreakCumulativeTimes[i][0] < eventData.milliseconds && eventData.milliseconds < _adBreakCumulativeTimes[i][1]) {
                    isVideoProgress = false;
                    cumulativeAdTimeIndex = i;
                    break;
                }
            }
            if (isVideoProgress) {
                if (_suppressSeekVideoProgress === false) {
                
                    // Send revenue/end if not yet sent
                    if (_playerState.currentAdBreak &&
                    _playerState.currentAdBreak.pod !== undefined &&
                    _playerState.currentAdBreak.end_beacon_sent === false) {
                    
                        _playerState.currentAdBreak.end_beacon_sent = true;
                        $htv.Beacons.trackRevenueEnd(_playerState.currentAdBreak, _playerState.currentAdBreak.duration, Math.round(_playerState.currentAdBreak.duration * 0.25));
                        
                        // send remaining audits
                        if (_playerState.currentAdBreak.audits !== undefined) {
                            for (i = 0; i < _playerState.currentAdBreak.audits.length; i++) {
                                if (_playerState.currentAdBreak.last_audited_pos < _playerState.currentAdBreak.audits[i].pos &&
                                _playerState.currentAdBreak.duration >= _playerState.currentAdBreak.audits[i].pos) {
                                    $htv.Beacons.trackAuditURL(_playerState.currentAdBreak.audits[i].url);
                                }
                            }
                        }
                    }

                    cumulativeTimeOffset = 0;
                    timeProgress = 0;
                    for (i = 0; i < _adBreakCumulativeTimes.length; i++) {
                        // LOG([_adBreakCumulativeTimes[i][1],_adBreakCumulativeTimes[i][0],eventData.milliseconds])
                        if (_adBreakCumulativeTimes[i][1] < eventData.milliseconds) {
                            cumulativeTimeOffset += (_adBreakCumulativeTimes[i][1] - _adBreakCumulativeTimes[i][0]);
                        } else {
                            break;
                        }
                    }
                    
                    timeProgress = eventData.milliseconds - cumulativeTimeOffset;
                    
                    if (_playerState.currentState !== "VIDEO") {
                        _playerState.currentState = "VIDEO";
                        $htv.Controller.fireEvent("PLAYER_STATE_CHANGED", {
                            state: _playerState.currentState
                        });
                        _updateStateUI();
                    }

                    if (_seekDestination !== -1) {
                        
                        $htv.Platform.seekTo(0, _seekDestination);
                        _seekDestination = -1;
                        
                        // Update progress bar
                        if (_playerState.resuming === true) {
                            $htv.Controller.fireEvent("PLAYER_RESUMING", {});
                            _playerState.resuming = false;
                        } else {
                            $htv.Controller.fireEvent("PLAYER_SEEKING", {});
                        }
                        
                        // Return so that bad video progress event isn't sent
                        return;
                    }
                    // if we have seek information and the seek is closer to the seek destination than to the seek origin, consider the seek complete
                    // however, if seek is under 20 seconds, cannot detect difference between start and finish so just assume it is complete
                    else if (_activeSeekInformation !== null &&
                        ((Math.abs(_activeSeekInformation.expected_destination - _activeSeekInformation.current_position) < 20000) || 
                        (Math.abs(timeProgress - _activeSeekInformation.expected_destination) < Math.abs(timeProgress - _activeSeekInformation.current_position)))) {
                        _activeSeekInformation.actual_destination = timeProgress;
                        $htv.Controller.fireEvent("PLAYER_SEEK_COMPLETED", _activeSeekInformation);
                        _activeSeekInformation = null;    
                    }
                    
                    _playerState.videoProgress = timeProgress;

                    if (_videoMetadata.end_credits_time !== 0 && _playerState.endCreditsHit === false &&
                    _videoMetadata.end_credits_time <= timeProgress) {
                        
                        $htv.Beacons.trackPlaybackPosition(true);
                        _playerState.endCreditsHit = true;
                        // record completion if not already sent
                        if (_playerState.completionRecorded === false) {
                            
                            $htv.Profile.recordVideoViewComplete(_videoMetadata.video_id);
                            _playerState.completionRecorded = true;
                        }
                        $htv.Controller.fireEvent("PLAYER_END_CREDITS_HIT");
                    }
                    
                    // record completion at 80% if not already sent
                    if (_playerState.completionRecorded === false && _playerState.videoProgress > 0.8 * 1000 * _videoMetadata.duration) {
                        
                        $htv.Profile.recordVideoViewComplete(_videoMetadata.video_id);
                        _playerState.completionRecorded = true;
                    }
                    
                    if (_playerState.size === STATE_END_CREDITS) {
                        resumeSeconds = Math.max(0, Math.round(_videoMetadata.duration - _playerState.videoProgress / 1000));
                        // short circuit asap so start at the end
                        for (i = _adBreakCumulativeTimes.length - 1; i >= 0; i++) {
                            if (_adBreakCumulativeTimes[i][0] > eventData.milliseconds) {
                                resumeSeconds += (_adBreakCumulativeTimes[i][1] - _adBreakCumulativeTimes[i][0]);
                            } else {
                                break;
                            }
                        }
                        _countdownText.setText((($htv.Profile.getPreference("autoplay_enabled") === true) ?
                            "Your next video will start in " :
                            "This video will end in ") +
                            $htv.Utils.formatTimeRemaining(resumeSeconds));                        
                    }
                    
                    $htv.Controller.fireEvent("PLAYER_TIME_PROGRESS", {
                        milliseconds: timeProgress,
                        bitrate: eventData.bitrate
                    });
                } // don't actually do anything if we're suppressing video progresses.
            } else {
            
                currentAdBreak = _findAdBreakByCumulativeTime(eventData.milliseconds);
                if (_playerState.currentAdBreak !== currentAdBreak) {
                    
                    _suppressSeekVideoProgress = false;
                    _playerState.accumulatedPlayback = 0;
                    // Send revenue/end if going from ad break to ad break
                    if (_playerState.currentAdBreak &&
                    _playerState.currentAdBreak.pod !== undefined &&
                    _playerState.currentAdBreak.end_beacon_sent === false) {
                    
                        _playerState.currentAdBreak.end_beacon_sent = true;
                        $htv.Beacons.trackRevenueEnd(_playerState.currentAdBreak, _playerState.currentAdBreak.duration, Math.round(_playerState.currentAdBreak.duration * 0.25));
                        
                        // send remaining audits
                        if (_playerState.currentAdBreak.audits !== undefined) {
                            for (i = 0; i < _playerState.currentAdBreak.audits.length; i++) {
                                if (_playerState.currentAdBreak.last_audited_pos < _playerState.currentAdBreak.audits[i].pos &&
                                _playerState.currentAdBreak.duration >= _playerState.currentAdBreak.audits[i].pos) {
                                    $htv.Beacons.trackAuditURL(_playerState.currentAdBreak.audits[i].url);
                                }
                            }
                        }
                    }
                    
                    _playerState.currentAdBreak = currentAdBreak;
                    _playerState.currentState = "BREAK";
                    $htv.Controller.fireEvent("PLAYER_STATE_CHANGED", {
                        state: _playerState.currentState,
                        breakpos: _playerState.currentAdBreak.pos
                    });
                    _updateStateUI();
                    
                    // Reset tracking vars
                    _playerState.currentAdBreak.last_beaconed_pos = 0;
                    _playerState.currentAdBreak.last_audited_pos = -1;
                    _playerState.currentAdBreak.end_beacon_sent = false;
                    
                    //Send playback position
                    $htv.Beacons.trackPlaybackPosition(true);
                    _updatePlaybackProgress(true);
                    
                    // Send revenue start
                    if (_playerState.currentAdBreak && _playerState.currentAdBreak.pod !== undefined) {
                        $htv.Beacons.trackRevenueStart(_playerState.currentAdBreak);
                    }
                }
                resumeSeconds = Math.max(0, Math.round((_adBreakCumulativeTimes[cumulativeAdTimeIndex][1] - eventData.milliseconds) / 1000));
                countdownText = "Ad";
                if (_playerState.currentAdBreak.group_slot_count > 1) {
                    countdownText += " " + (_playerState.currentAdBreak.group_slot + 1) + " of " + _playerState.currentAdBreak.group_slot_count;
                }
                countdownText += ": Your video will resume in " + $htv.Utils.formatTimeRemaining(resumeSeconds);
                _countdownText.setText(countdownText);
                
                // Compute ad progress by taking the progress of the current ad break group, subtracting previous breaks
                slotProgress = eventData.milliseconds - _adBreakCumulativeTimes[cumulativeAdTimeIndex][0];
                for (i = _playerState.currentAdBreak.breaks_index - 1; i >= 0; i--) {
                    if (_playlist.breaks[i].pos !== _playerState.currentAdBreak.pos) {
                        break;
                    }
                    slotProgress -= _playlist.breaks[i].duration;
                }
                
                // beacon revenue/position at 25/50/75 marks
                if (_playerState.currentAdBreak && _playerState.currentAdBreak.pod !== undefined) {
                    lastBeaconedPos = _playerState.currentAdBreak.last_beaconed_pos;
                    if ((lastBeaconedPos < _playerState.currentAdBreak.duration * 0.25 && slotProgress >= _playerState.currentAdBreak.duration * 0.25) ||
                    (lastBeaconedPos < _playerState.currentAdBreak.duration * 0.50 && slotProgress >= _playerState.currentAdBreak.duration * 0.50) ||
                    (lastBeaconedPos < _playerState.currentAdBreak.duration * 0.75 && slotProgress >= _playerState.currentAdBreak.duration * 0.75)) {
                        _playerState.currentAdBreak.last_beaconed_pos = slotProgress;
                        $htv.Beacons.trackRevenuePosition(_playerState.currentAdBreak, slotProgress, Math.round(_playerState.currentAdBreak.duration * 0.25));
                    }
                    
                    // send the third-party audits
                    if (_playerState.currentAdBreak.audits) {
                        for (i = 0; i < _playerState.currentAdBreak.audits.length; i++) {
                            if (_playerState.currentAdBreak.last_audited_pos < _playerState.currentAdBreak.audits[i].pos &&
                            slotProgress >= _playerState.currentAdBreak.audits[i].pos) {
                                $htv.Beacons.trackAuditURL(_playerState.currentAdBreak.audits[i].url);
                            }
                        }
                        _playerState.currentAdBreak.last_audited_pos = slotProgress;
                    }
                }
            }
        };
        
        _handlePlaybackEnded = function (playerId, eventType, eventData) {
            _playbackFinished();
        };
        
        _handleStateChanged = function (playerId, eventType, eventData) {
            describe(eventData);
            if (eventData.state === "BufferingComplete") {
                _hideBufferingUI();
            }
        };
        
        
        
    } else { // PROGRESSIVE
        _handleTimeProgress = function (playerId, eventType, eventData) {
            
            var i, lastBeaconedPos, nextBreak,  adBreakDuration, resumeSeconds, countdownText, timeProgress = 0;
            if (eventData.milliseconds > _videoMetadata.duration * 1000) {
                
                return;
            }
            
            timeProgress = eventData.milliseconds;
            if (_playerState.currentState === "VIDEO") {
                nextBreak = _fetchAdBreakFrom(_playerState.videoProgress);
                
                _playerState.videoProgress = timeProgress;
                
                if (_videoMetadata.end_credits_time !== 0 && _playerState.endCreditsHit === false &&
                _videoMetadata.end_credits_time <= timeProgress) {
                    
                    $htv.Beacons.trackPlaybackPosition(true);
                    _playerState.endCreditsHit = true;
                    // record completion if not already sent
                    if (_playerState.completionRecorded === false) {
                        
                        $htv.Profile.recordVideoViewComplete(_videoMetadata.video_id);
                        _playerState.completionRecorded = true;
                    }
                    $htv.Controller.fireEvent("PLAYER_END_CREDITS_HIT");
                }
                
                // record completion at 80% if not already sent
                if (_playerState.completionRecorded === false && _playerState.videoProgress > 0.8 * 1000 * _videoMetadata.duration) {
                    
                    $htv.Profile.recordVideoViewComplete(_videoMetadata.video_id);
                    _playerState.completionRecorded = true;
                }
                
                // if we have seek information and the seek is closer to the seek destination than to the seek origin, consider the seek complete
                // however, if seek is under 20 seconds, cannot detect difference between start and finish so just assume it is complete
                if (_activeSeekInformation !== null &&
                    ((Math.abs(_activeSeekInformation.expected_destination - _activeSeekInformation.current_position) < 20000) || 
                    (Math.abs(timeProgress - _activeSeekInformation.expected_destination) < Math.abs(timeProgress - _activeSeekInformation.current_position)))) {
                    _activeSeekInformation.actual_destination = timeProgress;
                    $htv.Controller.fireEvent("PLAYER_SEEK_COMPLETED", _activeSeekInformation);
                    _activeSeekInformation = null;    
                }
                
                if (nextBreak !== null) {
                    if (Math.floor(nextBreak.pos) <= timeProgress) {
                        
                        _playAdBreak(nextBreak);
                    }
                }
                
                if (_playerState.size === STATE_END_CREDITS) {
                    resumeSeconds = Math.max(0, Math.round(_videoMetadata.duration - _playerState.videoProgress / 1000));
                    // short circuit asap so start at the end
                    // TODO: consider the other variables on the ad breaks
                    for (i = _playlist.breaks.length - 1; i >= 0; i++) {
                        // if no duration
                        // TODO: finish comment
                        if (_playlist.breaks[i] && _playlist.breaks[i].pos > _playerState.videoProgress) {
                            if (_playlist.breaks[i].hasOwnProperty("duration")) {
                                resumeSeconds += (_playlist.breaks[i].duration);
                            } else {
                                resumeSeconds = -1;
                                break;
                            }
                        }
                        else {
                            break;    
                        }
                    }
                    if (resumeSeconds === -1) {
                        _countdownText.setText(($htv.Profile.getPreference("autoplay_enabled") === true) ?
                            "Your next video will start shortly." :
                            "This video will end shortly.");
                    }
                    else {
                        _countdownText.setText((($htv.Profile.getPreference("autoplay_enabled") === true) ?
                            "Your next video will start in " :
                            "This video will end in ") +
                            $htv.Utils.formatTimeRemaining(resumeSeconds));    
                    }
                }
                
                // Update progress bar
                if (_playerState.resuming === true) {
                    $htv.Controller.fireEvent("PLAYER_RESUMING", {});
                    _playerState.resuming = false;
                }
                $htv.Controller.fireEvent("PLAYER_TIME_PROGRESS", {
                    milliseconds: timeProgress
                });
            } else if (_playerState.currentState === "BREAK") {
                if (_playerState.currentAdBreak && _playerState.currentAdBreak.hasOwnProperty("duration")) {
                    adBreakDuration = _getTimeLeftForAdsAt(_playerState.currentAdBreak.pos, _playerState.currentAdBreak.group_slot);
                    resumeSeconds = Math.max(0, Math.min(Math.round(adBreakDuration / 1000), Math.round(adBreakDuration / 1000 - timeProgress / 1000)));
                    countdownText = "Ad";
                    if (_playerState.currentAdBreak.group_slot_count > 1) {
                        countdownText += " " + (_playerState.currentAdBreak.group_slot + 1) + " of " + _playerState.currentAdBreak.group_slot_count;
                    }
                    countdownText += ": Your video will resume in " + $htv.Utils.formatTimeRemaining(resumeSeconds);
                    _countdownText.setText(countdownText);
                } else {
                    _countdownText.setText("Ad: Your video will resume shortly");
                }
                
                // beacon revenue/position at 25/50/75 marks
                if (_playerState.currentAdBreak.pod !== undefined) {
                    lastBeaconedPos = _playerState.currentAdBreak.last_beaconed_pos;
                    if ((lastBeaconedPos < _playerState.currentAdBreak.duration * 0.25 && timeProgress >= _playerState.currentAdBreak.duration * 0.25) ||
                    (lastBeaconedPos < _playerState.currentAdBreak.duration * 0.50 && timeProgress >= _playerState.currentAdBreak.duration * 0.50) ||
                    (lastBeaconedPos < _playerState.currentAdBreak.duration * 0.75 && timeProgress >= _playerState.currentAdBreak.duration * 0.75)) {
                        _playerState.currentAdBreak.last_beaconed_pos = timeProgress;
                        $htv.Beacons.trackRevenuePosition(_playerState.currentAdBreak, timeProgress, Math.round(_playerState.currentAdBreak.duration * 0.25));
                    }
                    
                    // send the third-party audits
                    if (_playerState.currentAdBreak.audits !== undefined) {
                        for (i = 0; i < _playerState.currentAdBreak.audits.length; i++) {
                            if (_playerState.currentAdBreak.last_audited_pos < _playerState.currentAdBreak.audits[i].pos &&
                            timeProgress >= _playerState.currentAdBreak.audits[i].pos) {
                                $htv.Beacons.trackAuditURL(_playerState.currentAdBreak.audits[i].url);
                            }
                        }
                        _playerState.currentAdBreak.last_audited_pos = timeProgress;
                    }
                }
            }
        };
        
        _handleStateChanged = function (playerId, eventType, eventData) {
            var items, message, eventParams;
            if (eventData.state === "BufferingStarted") {
                if (_playerState.currentState === "VIDEO") {
                    
                    /*
                     _decreaseBitrate();
                     */
                    if (eventData.excessiveBuffering && !_hasShownBufferingDialog && _playerState.size === STATE_MAXIMIZED) {
                        message = "You appear to be having difficulty buffering video at the current quality setting. " +
                        "You can make adjustments by choosing the Video Quality link under the playback timeline.";
                        items = [{
                            text: "Change Now",
                            callback: function () {
                                $htv.Controller.resizePlayer($htv.Player.STATE_MAXIMIZED, {
                                    start_with_quality: true
                                });
                            },
                            receiver: this
                        }, {
                            text: "OK",
                            callback: function () {
                                $htv.Controller.resizePlayer($htv.Player.STATE_MAXIMIZED);
                                _resume(true);
                            },
                            receiver: this
                        }];
                        
                        // Hide player and show dialog 
                        _hasShownBufferingDialog = true;
                        _pause();
                        $htv.Controller.resizePlayer($htv.Player.STATE_MINIMIZED);
                        $htv.Controller.pushView("DialogBoxView", {
                            text: message,
                            items: items,
                            horizontalButtons: true,
                            textXPadding: 40,
                            textWidth: 480,
                            buttonYInitial: 310,
                            buttonWidth: 140,
                            return_closure: {
                                callback: function () {
                                    $htv.Controller.resizePlayer($htv.Player.STATE_MAXIMIZED);
                                    _resume(true);
                                },
                                receiver: this
                            }
                        });
                    }
                }
            } else if (eventData.state === "BufferingComplete") {
                _hideBufferingUI();
                if (_playerState.currentState === "VIDEO") {
                    
                } else if (_playerState.currentState === "SWITCHING_BITRATE") {
                    _playerState.currentState = "VIDEO";
                } else if (_playerState.currentState === "RESUMING_VIDEO") {
                    
                    _playerState.currentState = "VIDEO";
                    _playerState.lastVideoProgress = _playerState.videoProgress;
                    _playerState.accumulatedPlayback = 0;
                    _updateStateUI();
                    _disableAdBreaks();
                } else if (_playerState.currentState === "STARTING_VIDEO") {
                    
                    
                    _playerState.currentState = "VIDEO";
                    _updateStateUI();
                } else if (_playerState.currentState === "BREAK") {
                    _updateStateUI();
                }
                eventParams = {
                    state: _playerState.currentState
                };
                if (_playerState.currentState === "BREAK") {
                    eventParams.breakpos = _playerState.currentAdBreak.pos;
                }
                $htv.Controller.fireEvent("PLAYER_STATE_CHANGED", eventParams);
            }
        };
        
        _handlePlaybackEnded = function (playerId, eventType, eventData) {
            
            var i, nextBreak = null;
            
            // If we haven't watched all the ads at the pos where we just finished watching an ad, return another ad from that same position.
            if (_playerState.currentState === "BREAK" && _playerState.currentAdBreak && _moreAdsAtPos(_playerState.currentAdBreak.pos)) {
                
                describe(_playerState.currentAdBreak);
                nextBreak = _fetchAdBreakFrom(_playerState.currentAdBreak.pos);
            }            // Return the next ad starting from the current position of the video.
            else {
                
                nextBreak = _fetchAdBreakFrom(_playerState.videoProgress);
            }
            
            // If an ad just finished
            if (_playerState.currentState === "BREAK") {
                _playerState.accumulatedPlayback = 0;
                
                // beacon revenue/end
                if (_playerState.currentAdBreak.pod !== undefined) {
                    $htv.Beacons.trackRevenueEnd(_playerState.currentAdBreak, _playerState.currentAdBreak.duration, Math.round(_playerState.currentAdBreak.duration * 0.25));
                    
                    // send remaining audits
                    if (_playerState.currentAdBreak.audits !== undefined) {
                        for (i = 0; i < _playerState.currentAdBreak.audits.length; i++) {
                            if (_playerState.currentAdBreak.last_audited_pos < _playerState.currentAdBreak.audits[i].pos &&
                            _playerState.currentAdBreak.duration >= _playerState.currentAdBreak.audits[i].pos) {
                                $htv.Beacons.trackAuditURL(_playerState.currentAdBreak.audits[i].url);
                            }
                        }
                    }
                }
                
                // If two ads per pod
                if (nextBreak !== null && Math.floor(nextBreak.pos) <= _playerState.videoProgress) {
                    
                    _playAdBreak(nextBreak);
                }                // If was a postroll ad
                else if (_playerState.videoProgress >= _videoMetadata.duration * 1000) {
                    _playbackFinished();
                }                // Otherwise an interstitial ad
                else {
                    _playerState.currentState = "RESUMING_VIDEO";
                    if (_playerState.waitingToSwitch) {
                        _loadPlaylist(_videoMetadata.content_id, _playerState.pendingBitrate);
                        _playerState.waitingToSwitch = false;
                        _updateStateUI();
                    } else {
                        _playFile(_playlist.stream_url, _playerState.videoProgress, _playlist.aspect_ratio, _playerState.currentBitrate);
                    }
                    _showBufferingUI();
                }
                // If content finished and there is a post-roll ad
            } else if (nextBreak !== null) {
                
                _playerState.videoProgress = _videoMetadata.duration * 1000;
                _playAdBreak(nextBreak);
            }            // If content finished and there's no post-roll ad
            else {
                _playerState.videoProgress = _videoMetadata.duration * 1000;
                _playbackFinished();
            }
        };
        
    }
    
    
    function _enforceAdFreePeriod() {
        if (_playerState.currentState === "VIDEO") {
            if (_seeking === true) {
                _playerState.lastVideoProgress = _playerState.videoProgress;
                _seeking = false;
            }
            
            _playerState.lastAccumulatedPlayback = _playerState.accumulatedPlayback;
            _playerState.accumulatedPlayback += _playerState.videoProgress - _playerState.lastVideoProgress;
            _playerState.lastVideoProgress = _playerState.videoProgress;
            if (_playerState.lastAccumulatedPlayback < $htv.Constants.AD_FREE_SEEK_PERIOD && _playerState.accumulatedPlayback >= $htv.Constants.AD_FREE_SEEK_PERIOD) {
                _enableAdBreaks();
            }
        }
    }
    
    function _handlePlayerEvent(playerId, eventType, eventData) {
        var timeProgress, nextBreak;
        
        timeProgress = 0;
        switch (eventType) {
        case "TIME_PROGRESS":
            // 
            _handleTimeProgress(playerId, eventType, eventData);
            _enforceAdFreePeriod();
            // 
            break;
        case "PLAYBACK_STARTED":
            break;
        case "PLAYBACK_ENDED":
            
            
            _handlePlaybackEnded(playerId, eventType, eventData);
            break;
        case "STREAM_INFO_READY":
            // CAUTION: this might not be available on all platforms.
            describe(eventData);
            break;
        case "STATE_CHANGE":
            
            _handleStateChanged(playerId, eventType, eventData);
            break;
        case "RENDERING_ERROR":
            _playbackError("We're sorry, but there was an issue while playing this video. Please check your connection and try again.", "PLAYER_RENDERING_ERROR", eventData);
            break;
        case "PLAYBACK_ERROR":
            _playbackError("We're sorry, but there was an issue while playing this video. Please check your connection and try again.", "PLAYER_PLAYBACK_ERROR", eventData);
            break;
        case "STREAM_NOT_FOUND":
            _playbackError("We're sorry, but there was an issue while playing this video. Please check your connection and try again.", "PLAYER_STREAM_NOT_FOUND", eventData);
            break;
        case "CONNECTION_ERROR":
        case "NETWORK_DISCONNECTED":
        case "CONNECTION_FAILED":
            _playbackError("We're sorry, but there was a network connection issue while playing this video. Please check your connection and try again.", "PLAYER_CONNECTION_ERROR", eventData);
            break;
        default:
            
            break;
        }
    }
    
    function _playbackError(message, beaconType, beaconData) {
        var timeProgress, nextBreak, items, retryContentId, retryStartOptions;
        
        
        describe(beaconData);
        
        items = [{
            text: "OK",
            callback: function () {
            },
            receiver: this
        }];
        
        if (_videoMetadata && _videoMetadata.content_id > 0) {
            retryContentId = _videoMetadata.content_id;
            retryStartOptions = _startOptions;
            items.push({
                text: "Retry",
                callback: function () {
                    $htv.Controller.playVideo(retryContentId, retryStartOptions);
                },
                receiver: this
            });
        }
        
        // Hide player and show dialog 
        _stop({force_stop: true});
        $htv.Controller.pushView("DialogBoxView", {
            text: message,
            items: items,
            horizontalButtons: true,
            textXPadding: 70,
            textWidth: 420,
            buttonYInitial: 300
        });
        
        // send out a beacon
        $htv.Controller.fireEvent(beaconType, beaconData);
    }
    
    function _shouldShowEndCredits() {
        return _playerState.endCreditsHit === true && _playerState.endCreditsDismissed === false;
    }
        
    function _dismissEndCredits() {
        _playerState.endCreditsDismissed = true;
    }
    
    function _playbackFinished(options) {
        var eventData = {next_video: _nextVideoMetadata, next_video_type: _startOptions.next_video_type};
        if (_startOptions && _startOptions.disable_autoplay === true) {
            
            eventData.force_stop = true;
        }
        
        _playerTimer.stop();
        _updatePlaybackProgress(true); // save playback progress..
        $htv.Beacons.trackPlaybackPosition(!_playerState.paused);
        // fire playback banya call (type 1) using playlist view_token
        // if there's no view_token, it's not plus_only content, so we don't need to fire.
        if (_playlist.view_token) {
            $htv.Controller.makeBanyaRequest(1, _playlist.view_token);
        }
        $htv.Beacons.trackPlaybackEnd();
        eventData.final_size = _playerState.size;
        _resetPlaybackState();
        _hide();
        _hideBufferingUI();
        // do cleanup actions.. return to the original view.
        $htv.Platform.releasePlayer();
        if (options && options.force_stop === true) {
            eventData.force_stop = true;
        }
        if (options && options.force_autoplay === true) {
            eventData.force_autoplay = true;
        }
        $htv.Controller.fireEvent("PLAYER_PLAYBACK_FINISHED", eventData);
    }
    
    function _switchStream(stream) {
        if ($htv.Platform.properties.livestreaming === true) {
            $htv.Controller.fireEvent("PLAYER_QUALITY_CHANGED", {});
            $htv.Platform.switchBitrate(stream);
        } else {
            if (_playerState.currentState === "BREAK") {
                if (stream !== _playerState.currentBitrate) {
                    _playerState.pendingBitrate = stream;
                    _playerState.waitingToSwitch = true;
                }
            } else {
                if (stream === "AUTO") {
                    // do nothing
                } else {
                    if (stream !== _playerState.currentBitrate) {
                        $htv.Platform.stopVideo();
                        _showBufferingUI(0);
                        _loadPlaylist(_videoMetadata.content_id, stream);
                    }
                }
            }
        }
    }
    
    function _isPaused() {
        return _playerState.paused;
    }
    
    function _pause() {
        _updatePlaybackProgress(true);
        _playerState.paused = true;
        $htv.Platform.pauseVideo();
        $htv.Controller.fireEvent("PLAYER_PAUSED", {});
    }
    
    function _resume(doPlatformResume) {
        _playerState.paused = false;
        // update state even if we don't actually do the resume (for cases of seek).. 
        // todo, could duplicate the logic for state in seek.. or have seek call this method.
        if (doPlatformResume !== false) {
            $htv.Platform.resumeVideo();
        }
        $htv.Controller.fireEvent("PLAYER_PLAYING", {});
    }
    
    function _stop(options) {
        if (_playerState.currentState !== "INIT") {
            $htv.Platform.stopVideo();
            _playbackFinished(options);
        } else {
            
        }
    }
    
    function _playFile(url, startTime, aspectRatio, startingBitrate) {
        // NOTE: startingBitrate may be unused for progressive playback
        $htv.Platform.playVideo(url, startTime, startingBitrate);
        if (_playerState.aspectRatio !== aspectRatio) {
            _playerState.aspectRatio = aspectRatio;
            _enforcePlayerSize();
        }
    }
    
    function _enforcePlayerSize() {
        var newWidth, sizeRatio, position = {}, loadingDotsVisible = false, countDownVisible = false;
    
        if (_playerState && _playerState.size === STATE_MINIMIZED) {
            
            position.x = 782;
            position.y = 0;
            position.height = 100;
            loadingDotsVisible = false;
            countDownVisible = false;
        }
        else if (_playerState && _playerState.size === STATE_END_CREDITS) {
            
            position.x = 40;
            position.y = 30;
            position.height = 270;
            loadingDotsVisible = false;
        }
        else if (_playerState && _playerState.size === STATE_MAXIMIZED) {
            
            position.x = 0;
            position.y = 0;
            position.height = 540;
            loadingDotsVisible = true;
        }
        
        position.width = (position.height * 16) / 9;
        _playerState.position = position;
        sizeRatio = position.height / 540;
        
        if (_loadingImage !== null) {
            _loadingImage.move({
                x: position.x,
                y: position.y,
                z: 5,
                width: position.width,
                height: position.height
            });
            if (_loadingImageEnabled === true) {
                if (loadingDotsVisible === true) {
                    _loadingIndicator.show();    
                }
                else {
                    _loadingIndicator.hide();
                }
            }
        }
        
        if (_countdownBackground !== null) {
            _countdownBackground.move({
                x: position.x,
                y: position.y,
                z: 11,
                width: position.width,
                height: 31 
            });
            if (countDownVisible === true) {
                _countdownBackground.show();
            }
            else {
                _countdownBackground.hide();
            }
        }
        
        if (_countdownText !== null) {
            _countdownText.move({
                x: position.x + 15 * sizeRatio,
                y: position.y + 5,
                z: 12,
                width: position.width,
                height: 20 
            });
            if (countDownVisible === true) {
                _countdownText.show();
            }
            else {
                _countdownText.hide();
            }
        }              

        // use _playlist.aspect_ratio because the bugs are always based on content
        if (_dynamicBugBL !== null) {
            _dynamicBugBL.move({
                x: position.x + (_playlist.aspect_ratio === "4x3" ? (120 * sizeRatio) : 0) + (BUG_EDGE_OFFSET * sizeRatio),
                y: position.y + position.height - ((BUG_WIDTH_HEIGHT + BUG_EDGE_OFFSET) * sizeRatio),
                z: 11,
                width: BUG_WIDTH_HEIGHT * sizeRatio,
                height: BUG_WIDTH_HEIGHT * sizeRatio
            });
        }
        
        if (_dynamicBugBR !== null) {
            _dynamicBugBR.move({
                x: position.x + position.width - (_playlist.aspect_ratio === "4x3" ? (120 * sizeRatio) : 0) - ((BUG_WIDTH_HEIGHT + BUG_EDGE_OFFSET) * sizeRatio),
                y: position.y + position.height - ((BUG_WIDTH_HEIGHT + BUG_EDGE_OFFSET) * sizeRatio),
                z: 11,
                width: BUG_WIDTH_HEIGHT * sizeRatio,
                height: BUG_WIDTH_HEIGHT * sizeRatio
            });
        }

        // fix player position for aspect ratio
        // use _playerState.aspectRatio which is updated for ads        
        if (_playerState.aspectRatio === "4x3") {
            newWidth = (position.height * 4) / 3;
            position.x += (position.width - newWidth) / 2;
            position.width = newWidth;            
        }

        $htv.Platform.resizePlayer(position, 400);
    }
    
    function _resize(newState, force_resize) {
        if (newState === STATE_MAXIMIZED && _playerState.currentState === "INIT") {
            
            return;
        }
        if (_playerState.size !== newState || force_resize === true) {
            _playerState.size = newState;
            _enforcePlayerSize();
        }
        
        _updateStateUI();
    }

    function _hide() {              
        
        if (_dynamicBugBL !== null) {
            _dynamicBugBL.hide();
        }
        
        if (_dynamicBugBR !== null) {
            _dynamicBugBR.hide();
        }
        
        if (_countdownBackground !== null) {
            _countdownBackground.hide();
        }
        
        if (_countdownText !== null) {
            _countdownText.hide();
        }
        
        
        _hideBufferingUI();
        
    }
    
    function _show() {
        
    }
    
    function _discard() {
        
        _hide();
    }
    
    function _isActive() {
        return (_playerState.currentState !== "INIT");
    }
    
    function _currentSize() {
        return _playerState.size;
    }
    
    function _getViewToken() {
        return _playlist.view_token;
    }
    
    // generate fields from _playlist.  alternatively, populate a static blob
    function _getPlaylistData() {
        return {
            transcripts: _playlist.transcripts,
            view_token: _playlist.view_token
        };
    }
    
    function _getState() {
        return _playerState.currentState;
    }
    
    function _getAccumulatedPlaybackTime() {
        return _playerState.accumulatedPlayback;
    }
    
    function _getMetadata() {
        return _videoMetadata;
    }
    
    function _getNextVideoMetadata() {
        return _nextVideoMetadata;
    }
    
    // _loadEndpoints();
    
    
    return {
        discard: _discard,
        handleEvent: _handleEvent,
        handlePlayerEvent: _handlePlayerEvent,
        getViewToken: _getViewToken,
        resize: _resize,
        increaseBitrate: _increaseBitrate,
        decreaseBitrate: _decreaseBitrate,
        isActive: _isActive,
        instantReplay: _instantReplay,
        currentSize: _currentSize,
        getPlaylistData: _getPlaylistData,
        getState: _getState,
        getMetadata: _getMetadata,
        getNextVideoMetadata: _getNextVideoMetadata,
        getAccumulatedPlaybackTime: _getAccumulatedPlaybackTime,
        playVideo: _playVideo,
        pauseVideo: _pause,
        seekTo: _seekTo,
        resumeVideo: _resume,
        stopVideo: _stop,
        switchStream: _switchStream,
        updatePlaybackProgress: _updatePlaybackProgress,
        isPaused: _isPaused,
        shouldShowEndCredits: _shouldShowEndCredits,
        dismissEndCredits: _dismissEndCredits,
        STATE_MAXIMIZED: STATE_MAXIMIZED,
        STATE_MINIMIZED: STATE_MINIMIZED,
        STATE_END_CREDITS: STATE_END_CREDITS
    };
}());
