mediaControlsApple.js   [plain text]


function createControls(root, video, host)
{
    return new Controller(root, video, host);
};

function Controller(root, video, host)
{
    this.video = video;
    this.root = root;
    this.host = host;
    this.controls = {};
    this.listeners = {};
    this.isLive = false;
    this.statusHidden = true;
    this.hasWirelessPlaybackTargets = false;
    this.canToggleShowControlsButton = false;
    this.isListeningForPlaybackTargetAvailabilityEvent = false;
    this.currentTargetIsWireless = false;
    this.wirelessPlaybackDisabled = false;
    this.isVolumeSliderActive = false;
    this.currentDisplayWidth = 0;
    this._scrubbing = false;
    this._pageScaleFactor = 1;

    this.addVideoListeners();
    this.createBase();
    this.createControls();
    this.createTimeClones();
    this.updateBase();
    this.updateControls();
    this.updateDuration();
    this.updateProgress();
    this.updateTime();
    this.updateReadyState();
    this.updatePlaying();
    this.updateThumbnail();
    this.updateCaptionButton();
    this.updateCaptionContainer();
    this.updateFullscreenButtons();
    this.updateVolume();
    this.updateHasAudio();
    this.updateHasVideo();
    this.updateWirelessTargetAvailable();
    this.updateWirelessPlaybackStatus();
    this.updatePictureInPicturePlaceholder();
    this.scheduleUpdateLayoutForDisplayedWidth();

    this.listenFor(this.root, 'resize', this.handleRootResize);
};

/* Enums */
Controller.InlineControls = 0;
Controller.FullScreenControls = 1;

Controller.PlayAfterSeeking = 0;
Controller.PauseAfterSeeking = 1;

/* Globals */
Controller.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
Controller.gSimulatePictureInPictureAvailable = false; // Used for testing when picture-in-picture is not available.

Controller.prototype = {

    /* Constants */
    HandledVideoEvents: {
        loadstart: 'handleLoadStart',
        error: 'handleError',
        abort: 'handleAbort',
        suspend: 'handleSuspend',
        stalled: 'handleStalled',
        waiting: 'handleWaiting',
        emptied: 'handleReadyStateChange',
        loadedmetadata: 'handleReadyStateChange',
        loadeddata: 'handleReadyStateChange',
        canplay: 'handleReadyStateChange',
        canplaythrough: 'handleReadyStateChange',
        timeupdate: 'handleTimeUpdate',
        durationchange: 'handleDurationChange',
        playing: 'handlePlay',
        pause: 'handlePause',
        progress: 'handleProgress',
        volumechange: 'handleVolumeChange',
        webkitfullscreenchange: 'handleFullscreenChange',
        webkitbeginfullscreen: 'handleFullscreenChange',
        webkitendfullscreen: 'handleFullscreenChange',
    },
    PlaceholderPollingDelay: 33,
    HideControlsDelay: 4 * 1000,
    RewindAmount: 30,
    MaximumSeekRate: 8,
    SeekDelay: 1500,
    ClassNames: {
        active: 'active',
        dropped: 'dropped',
        exit: 'exit',
        failed: 'failed',
        hidden: 'hidden',
        hiding: 'hiding',
        threeDigitTime: 'three-digit-time',
        fourDigitTime: 'four-digit-time',
        fiveDigitTime: 'five-digit-time',
        sixDigitTime: 'six-digit-time',
        list: 'list',
        muteBox: 'mute-box',
        muted: 'muted',
        paused: 'paused',
        pictureInPicture: 'picture-in-picture',
        playing: 'playing',
        returnFromPictureInPicture: 'return-from-picture-in-picture',
        selected: 'selected',
        show: 'show',
        small: 'small',
        thumbnail: 'thumbnail',
        thumbnailImage: 'thumbnail-image',
        thumbnailTrack: 'thumbnail-track',
        volumeBox: 'volume-box',
        noVideo: 'no-video',
        down: 'down',
        out: 'out',
        pictureInPictureButton: 'picture-in-picture-button',
        placeholderShowing: 'placeholder-showing',
        usesLTRUserInterfaceLayoutDirection: 'uses-ltr-user-interface-layout-direction',
        appleTV: 'appletv',
    },
    KeyCodes: {
        enter: 13,
        escape: 27,
        space: 32,
        pageUp: 33,
        pageDown: 34,
        end: 35,
        home: 36,
        left: 37,
        up: 38,
        right: 39,
        down: 40
    },
    MinimumTimelineWidth: 80,
    ButtonWidth: 32,

    extend: function(child)
    {
        // This function doesn't actually do what we want it to. In particular it
        // is not copying the getters and setters to the child class, since they are
        // not enumerable. What we should do is use ES6 classes, or assign the __proto__
        // directly.
        // FIXME: Use ES6 classes.

        for (var property in this) {
            if (!child.hasOwnProperty(property))
                child[property] = this[property];
        }
    },

    get idiom()
    {
        return "apple";
    },

    UIString: function(developmentString, replaceString, replacementString)
    {
        var localized = UIStringTable[developmentString];
        if (replaceString && replacementString)
            return localized.replace(replaceString, replacementString);

        if (localized)
            return localized;

        console.error("Localization for string \"" + developmentString + "\" not found.");
        return "LOCALIZED STRING NOT FOUND";
    },

    listenFor: function(element, eventName, handler, useCapture)
    {
        if (typeof useCapture === 'undefined')
            useCapture = false;

        if (!(this.listeners[eventName] instanceof Array))
            this.listeners[eventName] = [];
        this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
        element.addEventListener(eventName, this, useCapture);
    },

    stopListeningFor: function(element, eventName, handler, useCapture)
    {
        if (typeof useCapture === 'undefined')
            useCapture = false;

        if (!(this.listeners[eventName] instanceof Array))
            return;

        this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
            return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
        });
        element.removeEventListener(eventName, this, useCapture);
    },

    addVideoListeners: function()
    {
        for (var name in this.HandledVideoEvents) {
            this.listenFor(this.video, name, this.HandledVideoEvents[name]);
        };

        /* text tracks */
        this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
        this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
        this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);

        /* audio tracks */
        this.listenFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
        this.listenFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
        this.listenFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);

        /* video tracks */
        this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo);
        this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
        this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);

        /* controls attribute */
        this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
        this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });

        this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);

        if ('webkitPresentationMode' in this.video)
            this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
    },

    removeVideoListeners: function()
    {
        for (var name in this.HandledVideoEvents) {
            this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
        };

        /* text tracks */
        this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
        this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
        this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);

        /* audio tracks */
        this.stopListeningFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
        this.stopListeningFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
        this.stopListeningFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);

        /* video tracks */
        this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo);
        this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
        this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);

        /* controls attribute */
        this.controlsObserver.disconnect();
        delete(this.controlsObserver);

        this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
        this.setShouldListenForPlaybackTargetAvailabilityEvent(false);

        if ('webkitPresentationMode' in this.video)
            this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
    },

    handleEvent: function(event)
    {
        var preventDefault = false;

        try {
            if (event.target === this.video) {
                var handlerName = this.HandledVideoEvents[event.type];
                var handler = this[handlerName];
                if (handler && handler instanceof Function)
                    handler.call(this, event);
            }

            if (!(this.listeners[event.type] instanceof Array))
                return;

            this.listeners[event.type].forEach(function(entry) {
                if (entry.element === event.currentTarget && entry.handler instanceof Function)
                    preventDefault |= entry.handler.call(this, event);
            }, this);
        } catch(e) {
            if (window.console)
                console.error(e);
        }

        if (preventDefault) {
            event.stopPropagation();
            event.preventDefault();
        }
    },

    createBase: function()
    {
        var base = this.base = document.createElement('div');
        base.setAttribute('pseudo', '-webkit-media-controls');
        this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
        this.listenFor(this.video, 'mouseout', this.handleWrapperMouseOut);
        if (this.host.textTrackContainer)
            base.appendChild(this.host.textTrackContainer);
    },

    shouldHaveAnyUI: function()
    {
        return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length) || this.currentPlaybackTargetIsWireless();
    },

    shouldShowControls: function()
    {
        if (!this.isAudio() && !this.host.allowsInlineMediaPlayback)
            return true;

        return this.video.controls || this.isFullScreen();
    },

    shouldHaveControls: function()
    {
        return this.shouldShowControls() || this.isFullScreen() || this.presentationMode() === 'picture-in-picture' || this.currentPlaybackTargetIsWireless();
    },
    

    setNeedsTimelineMetricsUpdate: function()
    {
        this.timelineMetricsNeedsUpdate = true;
    },

    scheduleUpdateLayoutForDisplayedWidth: function()
    {
        setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
    },

    updateTimelineMetricsIfNeeded: function()
    {
        if (this.timelineMetricsNeedsUpdate && !this.controlsAreHidden()) {
            this.timelineLeft = this.controls.timeline.offsetLeft;
            this.timelineWidth = this.controls.timeline.offsetWidth;
            this.timelineHeight = this.controls.timeline.offsetHeight;
            this.timelineMetricsNeedsUpdate = false;
        }
    },

    updateBase: function()
    {
        if (this.shouldHaveAnyUI()) {
            if (!this.base.parentNode) {
                this.root.appendChild(this.base);
            }
        } else {
            if (this.base.parentNode) {
                this.base.parentNode.removeChild(this.base);
            }
        }
    },

    createControls: function()
    {
        var panel = this.controls.panel = document.createElement('div');
        panel.setAttribute('pseudo', '-webkit-media-controls-panel');
        panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
        panel.setAttribute('role', 'toolbar');
        this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
        this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
        this.listenFor(panel, 'click', this.handlePanelClick);
        this.listenFor(panel, 'dblclick', this.handlePanelClick);
        this.listenFor(panel, 'dragstart', this.handlePanelDragStart);

        var panelBackgroundContainer = this.controls.panelBackgroundContainer = document.createElement('div');
        panelBackgroundContainer.setAttribute('pseudo', '-webkit-media-controls-panel-background-container');

        var panelTint = this.controls.panelTint = document.createElement('div');
        panelTint.setAttribute('pseudo', '-webkit-media-controls-panel-tint');
        this.listenFor(panelTint, 'mousedown', this.handlePanelMouseDown);
        this.listenFor(panelTint, 'transitionend', this.handlePanelTransitionEnd);
        this.listenFor(panelTint, 'click', this.handlePanelClick);
        this.listenFor(panelTint, 'dblclick', this.handlePanelClick);
        this.listenFor(panelTint, 'dragstart', this.handlePanelDragStart);

        var panelBackground = this.controls.panelBackground = document.createElement('div');
        panelBackground.setAttribute('pseudo', '-webkit-media-controls-panel-background');

        var rewindButton = this.controls.rewindButton = document.createElement('button');
        rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
        rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount));
        this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);

        var seekBackButton = this.controls.seekBackButton = document.createElement('button');
        seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
        seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
        this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
        this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);

        var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
        seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
        seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
        this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
        this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);

        var playButton = this.controls.playButton = document.createElement('button');
        playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
        playButton.setAttribute('aria-label', this.UIString('Play'));
        this.listenFor(playButton, 'click', this.handlePlayButtonClicked);

        var statusDisplay = this.controls.statusDisplay = document.createElement('div');
        statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
        statusDisplay.classList.add(this.ClassNames.hidden);

        var timelineBox = this.controls.timelineBox = document.createElement('div');
        timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');

        var currentTime = this.controls.currentTime = document.createElement('div');
        currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
        currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
        currentTime.setAttribute('role', 'timer');

        var timeline = this.controls.timeline = document.createElement('input');
        timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
        timeline.setAttribute('aria-label', this.UIString('Duration'));
        timeline.type = 'range';
        timeline.value = 0;
        this.listenFor(timeline, 'input', this.handleTimelineInput);
        this.listenFor(timeline, 'change', this.handleTimelineChange);
        this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
        this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
        this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
        this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
        this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
        this.listenFor(timeline, 'keydown', this.handleTimelineKeyDown);
        timeline.step = .01;

        this.timelineContextName = "_webkit-media-controls-timeline-" + this.host.generateUUID();
        timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';

        var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
        thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);

        var thumbnail = this.controls.thumbnail = document.createElement('div');
        thumbnail.classList.add(this.ClassNames.thumbnail);

        var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
        thumbnailImage.classList.add(this.ClassNames.thumbnailImage);

        var remainingTime = this.controls.remainingTime = document.createElement('div');
        remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
        remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
        remainingTime.setAttribute('role', 'timer');

        var muteBox = this.controls.muteBox = document.createElement('div');
        muteBox.classList.add(this.ClassNames.muteBox);
        this.listenFor(muteBox, 'mouseover', this.handleMuteBoxOver);

        var muteButton = this.controls.muteButton = document.createElement('button');
        muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
        muteButton.setAttribute('aria-label', this.UIString('Mute'));
        // Make the mute button a checkbox since it only has on/off states.
        muteButton.setAttribute('role', 'checkbox');
        this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);

        var minButton = this.controls.minButton = document.createElement('button');
        minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
        minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
        this.listenFor(minButton, 'click', this.handleMinButtonClicked);

        var maxButton = this.controls.maxButton = document.createElement('button');
        maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
        maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
        this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);

        var volumeBox = this.controls.volumeBox = document.createElement('div');
        volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
        volumeBox.classList.add(this.ClassNames.volumeBox);

        var volumeBoxBackground = this.controls.volumeBoxBackground = document.createElement('div');
        volumeBoxBackground.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-background');

        var volumeBoxTint = this.controls.volumeBoxTint = document.createElement('div');
        volumeBoxTint.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-tint');

        var volume = this.controls.volume = document.createElement('input');
        volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
        volume.setAttribute('aria-label', this.UIString('Volume'));
        volume.type = 'range';
        volume.min = 0;
        volume.max = 1;
        volume.step = .05;
        this.listenFor(volume, 'input', this.handleVolumeSliderInput);
        this.listenFor(volume, 'change', this.handleVolumeSliderChange);
        this.listenFor(volume, 'mousedown', this.handleVolumeSliderMouseDown);
        this.listenFor(volume, 'mouseup', this.handleVolumeSliderMouseUp);

        this.volumeContextName = "_webkit-media-controls-volume-" + this.host.generateUUID();
        volume.style.backgroundImage = '-webkit-canvas(' + this.volumeContextName + ')';

        var captionButton = this.controls.captionButton = document.createElement('button');
        captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
        captionButton.setAttribute('aria-label', this.UIString('Captions'));
        captionButton.setAttribute('aria-haspopup', 'true');
        captionButton.setAttribute('aria-owns', 'audioAndTextTrackMenu');
        this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);

        var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
        fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
        fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
        this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);

        var pictureInPictureButton = this.controls.pictureInPictureButton = document.createElement('button');
        pictureInPictureButton.setAttribute('pseudo', '-webkit-media-controls-picture-in-picture-button');
        pictureInPictureButton.setAttribute('aria-label', this.UIString('Display Picture in Picture'));
        pictureInPictureButton.classList.add(this.ClassNames.pictureInPictureButton);
        this.listenFor(pictureInPictureButton, 'click', this.handlePictureInPictureButtonClicked);

        var inlinePlaybackPlaceholder = this.controls.inlinePlaybackPlaceholder = document.createElement('div');
        inlinePlaybackPlaceholder.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-status');
        inlinePlaybackPlaceholder.setAttribute('aria-label', this.UIString('Video Playback Placeholder'));
        this.listenFor(inlinePlaybackPlaceholder, 'click', this.handlePlaceholderClick);
        this.listenFor(inlinePlaybackPlaceholder, 'dblclick', this.handlePlaceholderClick);
        if (!Controller.gSimulatePictureInPictureAvailable)
            inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);

        var inlinePlaybackPlaceholderText = this.controls.inlinePlaybackPlaceholderText = document.createElement('div');
        inlinePlaybackPlaceholderText.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text');

        var inlinePlaybackPlaceholderTextTop = this.controls.inlinePlaybackPlaceholderTextTop = document.createElement('p');
        inlinePlaybackPlaceholderTextTop.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-top');

        var inlinePlaybackPlaceholderTextBottom = this.controls.inlinePlaybackPlaceholderTextBottom = document.createElement('p');
        inlinePlaybackPlaceholderTextBottom.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-bottom');

        var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button');
        wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button');
        wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display'));
        this.listenFor(wirelessTargetPicker, 'click', this.handleWirelessPickerButtonClicked);

        // Show controls button is an accessibility workaround since the controls are now removed from the DOM. http://webkit.org/b/145684
        var showControlsButton = this.showControlsButton = document.createElement('button');
        showControlsButton.setAttribute('pseudo', '-webkit-media-show-controls');
        this.showShowControlsButton(false);
        showControlsButton.setAttribute('aria-label', this.UIString('Show Controls'));
        this.listenFor(showControlsButton, 'click', this.handleShowControlsClick);
        this.base.appendChild(showControlsButton);

        if (!Controller.gSimulateWirelessPlaybackTarget)
            wirelessTargetPicker.classList.add(this.ClassNames.hidden);
    },

    createTimeClones: function()
    {
        var currentTimeClone = this.currentTimeClone = document.createElement('div');
        currentTimeClone.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
        currentTimeClone.setAttribute('aria-hidden', 'true');
        currentTimeClone.classList.add('clone');
        this.base.appendChild(currentTimeClone);

        var remainingTimeClone = this.remainingTimeClone = document.createElement('div');
        remainingTimeClone.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
        remainingTimeClone.setAttribute('aria-hidden', 'true');
        remainingTimeClone.classList.add('clone');
        this.base.appendChild(remainingTimeClone);
    },

    setControlsType: function(type)
    {
        if (type === this.controlsType)
            return;
        this.controlsType = type;

        this.reconnectControls();
        this.updateShouldListenForPlaybackTargetAvailabilityEvent();
    },

    setIsLive: function(live)
    {
        if (live === this.isLive)
            return;
        this.isLive = live;

        this.updateStatusDisplay();

        this.reconnectControls();
    },

    reconnectControls: function()
    {
        this.disconnectControls();

        if (this.controlsType === Controller.InlineControls)
            this.configureInlineControls();
        else if (this.controlsType == Controller.FullScreenControls)
            this.configureFullScreenControls();
        if (this.shouldHaveControls() || this.currentPlaybackTargetIsWireless())
            this.addControls();
    },

    disconnectControls: function(event)
    {
        for (var item in this.controls) {
            var control = this.controls[item];
            if (control && control.parentNode)
                control.parentNode.removeChild(control);
       }
    },

    configureInlineControls: function()
    {
        this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
        this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
        this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
        this.controls.panel.appendChild(this.controls.panelBackgroundContainer);
        this.controls.panelBackgroundContainer.appendChild(this.controls.panelBackground);
        this.controls.panelBackgroundContainer.appendChild(this.controls.panelTint);
        this.controls.panel.appendChild(this.controls.playButton);
        if (!this.isLive)
            this.controls.panel.appendChild(this.controls.rewindButton);
        this.controls.panel.appendChild(this.controls.statusDisplay);
        if (!this.isLive) {
            this.controls.panel.appendChild(this.controls.timelineBox);
            this.controls.timelineBox.appendChild(this.controls.currentTime);
            this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
            this.controls.thumbnailTrack.appendChild(this.controls.timeline);
            this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
            this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
            this.controls.timelineBox.appendChild(this.controls.remainingTime);
        }
        this.controls.panel.appendChild(this.controls.muteBox);
        this.controls.muteBox.appendChild(this.controls.volumeBox);
        this.controls.volumeBox.appendChild(this.controls.volumeBoxBackground);
        this.controls.volumeBox.appendChild(this.controls.volumeBoxTint);
        this.controls.volumeBox.appendChild(this.controls.volume);
        this.controls.muteBox.appendChild(this.controls.muteButton);
        this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
        this.controls.panel.appendChild(this.controls.captionButton);
        if (!this.isAudio()) {
            this.updatePictureInPictureButton();
            this.controls.panel.appendChild(this.controls.fullscreenButton);
        }

        this.controls.panel.style.removeProperty('left');
        this.controls.panel.style.removeProperty('top');
        this.controls.panel.style.removeProperty('bottom');
    },

    configureFullScreenControls: function()
    {
        this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
        this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
        this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
        this.controls.panel.appendChild(this.controls.panelBackground);
        this.controls.panel.appendChild(this.controls.panelTint);
        this.controls.panel.appendChild(this.controls.volumeBox);
        this.controls.volumeBox.appendChild(this.controls.minButton);
        this.controls.volumeBox.appendChild(this.controls.volume);
        this.controls.volumeBox.appendChild(this.controls.maxButton);
        this.controls.panel.appendChild(this.controls.seekBackButton);
        this.controls.panel.appendChild(this.controls.playButton);
        this.controls.panel.appendChild(this.controls.seekForwardButton);
        this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
        this.controls.panel.appendChild(this.controls.captionButton);
        if (!this.isAudio()) {
            this.updatePictureInPictureButton();
            this.controls.panel.appendChild(this.controls.fullscreenButton);
        }
        if (!this.isLive) {
            this.controls.panel.appendChild(this.controls.timelineBox);
            this.controls.timelineBox.appendChild(this.controls.currentTime);
            this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
            this.controls.thumbnailTrack.appendChild(this.controls.timeline);
            this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
            this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
            this.controls.timelineBox.appendChild(this.controls.remainingTime);
        } else
            this.controls.panel.appendChild(this.controls.statusDisplay);
    },

    updateControls: function()
    {
        if (this.isFullScreen())
            this.setControlsType(Controller.FullScreenControls);
        else
            this.setControlsType(Controller.InlineControls);

        this.setNeedsUpdateForDisplayedWidth();
        this.updateLayoutForDisplayedWidth();
        this.setNeedsTimelineMetricsUpdate();

        if (this.shouldShowControls()) {
            this.controls.panel.classList.add(this.ClassNames.show);
            this.controls.panel.classList.remove(this.ClassNames.hidden);
            this.resetHideControlsTimer();
            this.showShowControlsButton(false);
        } else {
            this.controls.panel.classList.remove(this.ClassNames.show);
            this.controls.panel.classList.add(this.ClassNames.hidden);
            this.showShowControlsButton(true);
        }
    },

    isPlayable: function()
    {
        return this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error;
    },

    updateStatusDisplay: function(event)
    {
        this.updateShouldListenForPlaybackTargetAvailabilityEvent();
        if (this.video.error !== null)
            this.controls.statusDisplay.innerText = this.UIString('Error');
        else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA)
            this.controls.statusDisplay.innerText = this.UIString('Live Broadcast');
        else if (!this.isPlayable() && this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
            this.controls.statusDisplay.innerText = this.UIString('Loading');
        else
            this.controls.statusDisplay.innerText = '';

        this.setStatusHidden(!this.isLive && this.isPlayable());
    },

    handleLoadStart: function(event)
    {
        this.updateStatusDisplay();
        this.updateProgress();
    },

    handleError: function(event)
    {
        this.updateStatusDisplay();
    },

    handleAbort: function(event)
    {
        this.updateStatusDisplay();
    },

    handleSuspend: function(event)
    {
        this.updateStatusDisplay();
    },

    handleStalled: function(event)
    {
        this.updateStatusDisplay();
        this.updateProgress();
    },

    handleWaiting: function(event)
    {
        this.updateStatusDisplay();
    },

    handleReadyStateChange: function(event)
    {
        this.updateReadyState();
        this.updateDuration();
        this.updateCaptionButton();
        this.updateCaptionContainer();
        this.updateFullscreenButtons();
        this.updateWirelessTargetAvailable();
        this.updateWirelessTargetPickerButton();
        this.updateProgress();
        this.updateControls();
    },

    handleTimeUpdate: function(event)
    {
        if (!this.scrubbing) {
            this.updateTime();
            this.updateProgress();
        }
        this.drawTimelineBackground();
    },

    handleDurationChange: function(event)
    {
        this.updateDuration();
        this.updateTime();
        this.updateProgress();
    },

    handlePlay: function(event)
    {
        this.setPlaying(true);
    },

    handlePause: function(event)
    {
        this.setPlaying(false);
    },

    handleProgress: function(event)
    {
        this.updateProgress();
    },

    handleVolumeChange: function(event)
    {
        this.updateVolume();
    },

    handleTextTrackChange: function(event)
    {
        this.updateCaptionContainer();
    },

    handleTextTrackAdd: function(event)
    {
        var track = event.track;

        if (this.trackHasThumbnails(track) && track.mode === 'disabled')
            track.mode = 'hidden';

        this.updateThumbnail();
        this.updateCaptionButton();
        this.updateCaptionContainer();
    },

    handleTextTrackRemove: function(event)
    {
        this.updateThumbnail();
        this.updateCaptionButton();
        this.updateCaptionContainer();
    },

    handleAudioTrackChange: function(event)
    {
        this.updateHasAudio();
    },

    handleAudioTrackAdd: function(event)
    {
        this.updateHasAudio();
        this.updateCaptionButton();
    },

    handleAudioTrackRemove: function(event)
    {
        this.updateHasAudio();
        this.updateCaptionButton();
    },

    presentationMode: function() {
        if ('webkitPresentationMode' in this.video)
            return this.video.webkitPresentationMode;

        if (this.isFullScreen())
            return 'fullscreen';

        return 'inline';
    },

    isFullScreen: function()
    {
        if (!this.video.webkitDisplayingFullscreen)
            return false;

        if ('webkitPresentationMode' in this.video && this.video.webkitPresentationMode === 'picture-in-picture')
            return false;

        return true;
    },

    updatePictureInPictureButton: function()
    {
        var shouldShowPictureInPictureButton = (Controller.gSimulatePictureInPictureAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('picture-in-picture'))) && this.hasVideo();
        if (shouldShowPictureInPictureButton) {
            if (!this.controls.pictureInPictureButton.parentElement) {
                if (this.controls.fullscreenButton.parentElement == this.controls.panel)
                    this.controls.panel.insertBefore(this.controls.pictureInPictureButton, this.controls.fullscreenButton);
                else
                    this.controls.panel.appendChild(this.controls.pictureInPictureButton);
            }
            this.controls.pictureInPictureButton.classList.remove(this.ClassNames.hidden);
        } else
            this.controls.pictureInPictureButton.classList.add(this.ClassNames.hidden);
    },
    
    timelineStepFromVideoDuration: function()
    {
        var step;
        var duration = this.video.duration;
        if (duration <= 10)
            step = .5;
        else if (duration <= 60)
            step = 1;
        else if (duration <= 600)
            step = 10;
        else if (duration <= 3600)
            step = 30;
        else
            step = 60;
        
        return step;
    },
    
    incrementTimelineValue: function()
    {
        var value = this.video.currentTime + this.timelineStepFromVideoDuration();
        return value > this.video.duration ? this.video.duration : value;
    },

    decrementTimelineValue: function()
    {
        var value = this.video.currentTime - this.timelineStepFromVideoDuration();
        return value < 0 ? 0 : value;
    },

    showInlinePlaybackPlaceholderWhenSafe: function() {
        if (this.presentationMode() != 'picture-in-picture')
            return;

        if (!this.host.isVideoLayerInline) {
            this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
            this.base.classList.add(this.ClassNames.placeholderShowing);
        } else
            setTimeout(this.showInlinePlaybackPlaceholderWhenSafe.bind(this), this.PlaceholderPollingDelay);
    },

    shouldReturnVideoLayerToInline: function()
    {
        var presentationMode = this.presentationMode();
        return presentationMode === 'inline' || presentationMode === 'fullscreen';
    },

    updatePictureInPicturePlaceholder: function()
    {
        var presentationMode = this.presentationMode();

        switch (presentationMode) {
            case 'inline':
                this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
                this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
                this.base.classList.remove(this.ClassNames.placeholderShowing);

                this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
                break;
            case 'picture-in-picture':
                this.controls.panel.classList.add(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.pictureInPicture);
                this.showInlinePlaybackPlaceholderWhenSafe();

                this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.UIString('This video is playing in picture in picture.');
                this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholderTextBottom.innerText = "";
                this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.pictureInPicture);

                this.controls.pictureInPictureButton.classList.add(this.ClassNames.returnFromPictureInPicture);
                break;
            default:
                this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
                this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);

                this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
                break;
        }
    },

    handlePresentationModeChange: function(event)
    {
        this.updatePictureInPicturePlaceholder();
        this.updateControls();
        this.updateCaptionContainer();
        this.resetHideControlsTimer();
        if (this.presentationMode() != 'fullscreen' && this.video.paused && this.controlsAreHidden())
            this.showControls();
        this.host.setPreparedToReturnVideoLayerToInline(this.shouldReturnVideoLayerToInline());
    },

    handleFullscreenChange: function(event)
    {
        this.updateBase();
        this.updateControls();
        this.updateFullscreenButtons();
        this.updateWirelessPlaybackStatus();

        if (this.isFullScreen()) {
            this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
            this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
            this.host.enteredFullscreen();
        } else {
            this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
            this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
            this.host.exitedFullscreen();
        }

        if ('webkitPresentationMode' in this.video)
            this.handlePresentationModeChange(event);
    },

    handleShowControlsClick: function(event)
    {
        if (!this.video.controls && !this.isFullScreen())
            return;

        if (this.controlsAreHidden())
            this.showControls(true);
    },

    handleWrapperMouseMove: function(event)
    {
        if (!this.video.controls && !this.isFullScreen())
            return;

        if (this.controlsAreHidden())
            this.showControls();
        this.resetHideControlsTimer();

        if (!this.isDragging)
            return;
        var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
        this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
        this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
        event.stopPropagation()
    },

    handleWrapperMouseOut: function(event)
    {
        this.hideControls();
        this.clearHideControlsTimer();
    },

    handleWrapperMouseUp: function(event)
    {
        this.isDragging = false;
        this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
    },

    handlePanelMouseDown: function(event)
    {
        if (event.target != this.controls.panelTint && event.target != this.controls.inlinePlaybackPlaceholder)
            return;

        if (!this.isFullScreen())
            return;

        this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
        this.isDragging = true;
        this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
        this.initialOffset = new WebKitPoint(
            parseInt(this.controls.panel.style.left) | 0,
            parseInt(this.controls.panel.style.top) | 0
        );
    },

    handlePanelTransitionEnd: function(event)
    {
        var opacity = window.getComputedStyle(this.controls.panel).opacity;
        if (!parseInt(opacity) && !this.controlsAlwaysVisible() && (this.video.controls || this.isFullScreen())) {
            this.base.removeChild(this.controls.inlinePlaybackPlaceholder);
            this.base.removeChild(this.controls.panel);
        }
    },

    handlePanelClick: function(event)
    {
        // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
        event.preventDefault();
    },

    handlePanelDragStart: function(event)
    {
        // Prevent drags in the panel from triggering a drag event on the <video> element.
        event.preventDefault();
    },

    handlePlaceholderClick: function(event)
    {
        // Prevent clicks in the placeholder from playing or pausing the video in a MediaDocument.
        event.preventDefault();
    },

    handleRewindButtonClicked: function(event)
    {
        var newTime = Math.max(
                               this.video.currentTime - this.RewindAmount,
                               this.video.seekable.start(0));
        this.video.currentTime = newTime;
        return true;
    },

    canPlay: function()
    {
        return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
    },

    handlePlayButtonClicked: function(event)
    {
        if (this.canPlay()) {
            this.canToggleShowControlsButton = true;
            this.video.play();
        } else
            this.video.pause();
        return true;
    },

    handleTimelineInput: function(event)
    {
        if (this.scrubbing)
            this.video.pause();

        this.video.fastSeek(this.controls.timeline.value);
        this.updateControlsWhileScrubbing();
    },

    handleTimelineChange: function(event)
    {
        this.video.currentTime = this.controls.timeline.value;
        this.updateProgress();
    },

    handleTimelineDown: function(event)
    {
        this.controls.thumbnail.classList.add(this.ClassNames.show);
    },

    handleTimelineUp: function(event)
    {
        this.controls.thumbnail.classList.remove(this.ClassNames.show);
    },

    handleTimelineMouseOver: function(event)
    {
        this.controls.thumbnail.classList.add(this.ClassNames.show);
    },

    handleTimelineMouseOut: function(event)
    {
        this.controls.thumbnail.classList.remove(this.ClassNames.show);
    },

    handleTimelineMouseMove: function(event)
    {
        if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
            return;

        this.updateTimelineMetricsIfNeeded();
        this.controls.thumbnail.classList.add(this.ClassNames.show);
        var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
        var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
        percent = Math.max(Math.min(1, percent), 0);
        this.controls.thumbnail.style.left = percent * 100 + '%';

        var thumbnailTime = percent * this.video.duration;
        for (var i = 0; i < this.video.textTracks.length; ++i) {
            var track = this.video.textTracks[i];
            if (!this.trackHasThumbnails(track))
                continue;

            if (!track.cues)
                continue;

            for (var j = 0; j < track.cues.length; ++j) {
                var cue = track.cues[j];
                if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
                    this.controls.thumbnailImage.src = cue.text;
                    return;
                }
            }
        }
    },

    handleTimelineMouseDown: function(event)
    {
        this.scrubbing = true;
    },

    handleTimelineMouseUp: function(event)
    {
        this.scrubbing = false;
    },
    
    handleTimelineKeyDown: function(event)
    {
        if (event.keyCode == this.KeyCodes.left)
            this.controls.timeline.value = this.decrementTimelineValue();
        else if (event.keyCode == this.KeyCodes.right)
            this.controls.timeline.value = this.incrementTimelineValue();
    },

    handleMuteButtonClicked: function(event)
    {
        this.video.muted = !this.video.muted;
        if (this.video.muted)
            this.controls.muteButton.setAttribute('aria-checked', 'true');
        else
            this.controls.muteButton.setAttribute('aria-checked', 'false');
        this.drawVolumeBackground();
        return true;
    },

    handleMuteBoxOver: function(event)
    {
        this.drawVolumeBackground();
    },

    handleMinButtonClicked: function(event)
    {
        if (this.video.muted) {
            this.video.muted = false;
            this.controls.muteButton.setAttribute('aria-checked', 'false');
        }
        this.video.volume = 0;
        return true;
    },

    handleMaxButtonClicked: function(event)
    {
        if (this.video.muted) {
            this.video.muted = false;
            this.controls.muteButton.setAttribute('aria-checked', 'false');
        }
        this.video.volume = 1;
    },

    updateVideoVolume: function()
    {
        if (this.video.muted) {
            this.video.muted = false;
            this.controls.muteButton.setAttribute('aria-checked', 'false');
        }
        this.video.volume = this.controls.volume.value;
        this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
    },

    handleVolumeSliderInput: function(event)
    {
        this.updateVideoVolume();
        this.drawVolumeBackground();
    },
    
    handleVolumeSliderChange: function(event)
    {
        this.updateVideoVolume();
    },

    handleVolumeSliderMouseDown: function(event)
    {
        this.isVolumeSliderActive = true;
        this.drawVolumeBackground();
    },

    handleVolumeSliderMouseUp: function(event)
    {
        this.isVolumeSliderActive = false;
        this.drawVolumeBackground();
    },

    handleCaptionButtonClicked: function(event)
    {
        if (this.captionMenu)
            this.destroyCaptionMenu();
        else
            this.buildCaptionMenu();
        return true;
    },

    hasVideo: function()
    {
        return this.video.videoTracks && this.video.videoTracks.length;
    },

    updateFullscreenButtons: function()
    {
        var shouldBeHidden = !this.video.webkitSupportsFullscreen || !this.hasVideo();
        this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden && !this.isFullScreen());
        this.updatePictureInPictureButton();
        this.setNeedsUpdateForDisplayedWidth();
        this.updateLayoutForDisplayedWidth();
    },

    handleFullscreenButtonClicked: function(event)
    {
        if (this.isFullScreen())
            this.video.webkitExitFullscreen();
        else
            this.video.webkitEnterFullscreen();
        return true;
    },
    
    updateWirelessTargetPickerButton: function() {
        var wirelessTargetPickerColor;
        if (this.controls.wirelessTargetPicker.classList.contains('playing'))
            wirelessTargetPickerColor = "-apple-wireless-playback-target-active";
        else
            wirelessTargetPickerColor = "rgba(255,255,255,0.45)";
        if (window.devicePixelRatio == 2)
            this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,0.5 L 16,0.5 L 16,15.5 L 0,15.5 z M 0,14.5 L 16,14.5 L 8,5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 3.5,13.25 L 12.5,13.25 L 8,8 z'/></svg>\")";
        else
            this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,1 L 16,1 L 16,16 L 0,16 z M 0,15 L 16,15 L 8,5.5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2.5' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 2.75,14 L 13.25,14 L 8,8.75 z'/></svg>\")";
    },

    handleControlsChange: function()
    {
        try {
            this.updateBase();

            if (this.shouldHaveControls() && !this.hasControls())
                this.addControls();
            else if (!this.shouldHaveControls() && this.hasControls())
                this.removeControls();
        } catch(e) {
            if (window.console)
                console.error(e);
        }
    },

    nextRate: function()
    {
        return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
    },

    handleSeekBackMouseDown: function(event)
    {
        this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
        this.video.play();
        this.video.playbackRate = this.nextRate() * -1;
        this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
    },

    seekBackFaster: function()
    {
        this.video.playbackRate = this.nextRate() * -1;
    },

    handleSeekBackMouseUp: function(event)
    {
        this.video.playbackRate = this.video.defaultPlaybackRate;
        if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
            this.video.pause();
        else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
            this.video.play();
        if (this.seekInterval)
            clearInterval(this.seekInterval);
    },

    handleSeekForwardMouseDown: function(event)
    {
        this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
        this.video.play();
        this.video.playbackRate = this.nextRate();
        this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
    },

    seekForwardFaster: function()
    {
        this.video.playbackRate = this.nextRate();
    },

    handleSeekForwardMouseUp: function(event)
    {
        this.video.playbackRate = this.video.defaultPlaybackRate;
        if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
            this.video.pause();
        else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
            this.video.play();
        if (this.seekInterval)
            clearInterval(this.seekInterval);
    },

    updateDuration: function()
    {
        var duration = this.video.duration;
        this.controls.timeline.min = 0;
        this.controls.timeline.max = duration;

        this.setIsLive(duration === Number.POSITIVE_INFINITY);

        var timeControls = [this.controls.currentTime, this.controls.remainingTime, this.currentTimeClone, this.remainingTimeClone];

        function removeTimeClass(className) {
            for (let element of timeControls)
                element.classList.remove(className);
        }

        function addTimeClass(className) {
            for (let element of timeControls)
                element.classList.add(className);
        }

        // Reset existing style.
        removeTimeClass(this.ClassNames.threeDigitTime);
        removeTimeClass(this.ClassNames.fourDigitTime);
        removeTimeClass(this.ClassNames.fiveDigitTime);
        removeTimeClass(this.ClassNames.sixDigitTime);

        if (duration >= 60*60*10)
            addTimeClass(this.ClassNames.sixDigitTime);
        else if (duration >= 60*60)
            addTimeClass(this.ClassNames.fiveDigitTime);
        else if (duration >= 60*10)
            addTimeClass(this.ClassNames.fourDigitTime);
        else
            addTimeClass(this.ClassNames.threeDigitTime);
    },

    progressFillStyle: function(context)
    {
        var height = this.timelineHeight;
        var gradient = context.createLinearGradient(0, 0, 0, height);
        gradient.addColorStop(0, 'rgb(2, 2, 2)');
        gradient.addColorStop(1, 'rgb(23, 23, 23)');
        return gradient;
    },

    updateProgress: function()
    {
        this.updateTimelineMetricsIfNeeded();
        this.drawTimelineBackground();
    },

    addRoundedRect: function(ctx, x, y, width, height, radius) {
        ctx.moveTo(x + radius, y);
        ctx.arcTo(x + width, y, x + width, y + radius, radius);
        ctx.lineTo(x + width, y + height - radius);
        ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
        ctx.lineTo(x + radius, y + height);
        ctx.arcTo(x, y + height, x, y + height - radius, radius);
        ctx.lineTo(x, y + radius);
        ctx.arcTo(x, y, x + radius, y, radius);
    },

    drawTimelineBackground: function() {
        var dpr = window.devicePixelRatio;
        var width = this.timelineWidth * dpr;
        var height = this.timelineHeight * dpr;

        if (!width || !height)
            return;

        var played = this.controls.timeline.value / this.controls.timeline.max;
        var buffered = 0;
        for (var i = 0, end = this.video.buffered.length; i < end; ++i)
            buffered = Math.max(this.video.buffered.end(i), buffered);

        buffered /= this.video.duration;

        var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);

        width /= dpr;
        height /= dpr;

        ctx.save();
        ctx.scale(dpr, dpr);
        ctx.clearRect(0, 0, width, height);

        var timelineHeight = 3;
        var trackHeight = 1;
        var scrubberWidth = 3;
        var scrubberHeight = 15;
        var borderSize = 2;
        var scrubberPosition = Math.max(0, Math.min(width - scrubberWidth, Math.round(width * played)));

        // Draw buffered section.
        ctx.save();
        if (this.isAudio())
            ctx.fillStyle = "rgb(71, 71, 71)";
        else
            ctx.fillStyle = "rgb(30, 30, 30)";
        ctx.fillRect(1, 8, Math.round(width * buffered) - borderSize, trackHeight);
        ctx.restore();

        // Draw timeline border.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, scrubberPosition, 7, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
        this.addRoundedRect(ctx, scrubberPosition + 1, 8, width - scrubberPosition - borderSize , trackHeight, trackHeight / 2.0);
        ctx.closePath();
        ctx.clip("evenodd");
        if (this.isAudio())
            ctx.fillStyle = "rgb(71, 71, 71)";
        else
            ctx.fillStyle = "rgb(30, 30, 30)";
        ctx.fillRect(0, 0, width, height);
        ctx.restore();

        // Draw played section.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, 0, 7, width, timelineHeight, timelineHeight / 2.0);
        ctx.closePath();
        ctx.clip();
        if (this.isAudio())
            ctx.fillStyle = "rgb(116, 116, 116)";
        else
            ctx.fillStyle = "rgb(75, 75, 75)";
        ctx.fillRect(0, 0, width * played, height);
        ctx.restore();

        // Draw the scrubber.
        ctx.save();
        ctx.clearRect(scrubberPosition - 1, 0, scrubberWidth + borderSize, height, 0);
        ctx.beginPath();
        this.addRoundedRect(ctx, scrubberPosition, 1, scrubberWidth, scrubberHeight, 1);
        ctx.closePath();
        ctx.clip();
        if (this.isAudio())
            ctx.fillStyle = "rgb(181, 181, 181)";
        else
            ctx.fillStyle = "rgb(140, 140, 140)";
        ctx.fillRect(0, 0, width, height);
        ctx.restore();

        ctx.restore();
    },

    drawVolumeBackground: function() {
        var dpr = window.devicePixelRatio;
        var width = this.controls.volume.offsetWidth * dpr;
        var height = this.controls.volume.offsetHeight * dpr;

        if (!width || !height)
            return;

        var ctx = document.getCSSCanvasContext('2d', this.volumeContextName, width, height);

        width /= dpr;
        height /= dpr;

        ctx.save();
        ctx.scale(dpr, dpr);
        ctx.clearRect(0, 0, width, height);

        var seekerPosition = this.controls.volume.value;
        var trackHeight = 1;
        var timelineHeight = 3;
        var scrubberRadius = 3.5;
        var scrubberDiameter = 2 * scrubberRadius;
        var borderSize = 2;

        var scrubberPosition = Math.round(seekerPosition * (width - scrubberDiameter - borderSize));


        // Draw portion of volume under slider thumb.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, 0, 3, scrubberPosition + 2, timelineHeight, timelineHeight / 2.0);
        ctx.closePath();
        ctx.clip();
        ctx.fillStyle = "rgb(75, 75, 75)";
        ctx.fillRect(0, 0, width, height);
        ctx.restore();

        // Draw portion of volume above slider thumb.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, scrubberPosition, 3, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
        ctx.closePath();
        ctx.clip();
        ctx.fillStyle = "rgb(30, 30, 30)";
        ctx.fillRect(0, 0, width, height);
        ctx.restore();

        // Clear a hole in the slider for the scrubber.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, scrubberPosition, 0, scrubberDiameter + borderSize, height, (scrubberDiameter + borderSize) / 2.0);
        ctx.closePath();
        ctx.clip();
        ctx.clearRect(0, 0, width, height);
        ctx.restore();

        // Draw scrubber.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, scrubberPosition + 1, 1, scrubberDiameter, scrubberDiameter, scrubberRadius);
        ctx.closePath();
        ctx.clip();
        if (this.isVolumeSliderActive)
            ctx.fillStyle = "white";
        else
            ctx.fillStyle = "rgb(140, 140, 140)";
        ctx.fillRect(0, 0, width, height);
        ctx.restore();

        ctx.restore();
    },
    
    formatTimeDescription: function(time)
    {
        if (isNaN(time))
            time = 0;
        var absTime = Math.abs(time);
        var intSeconds = Math.floor(absTime % 60).toFixed(0);
        var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
        var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
        
        var secondString = intSeconds == 1 ? 'Second' : 'Seconds';
        var minuteString = intMinutes == 1 ? 'Minute' : 'Minutes';
        var hourString = intHours == 1 ? 'Hour' : 'Hours';
        if (intHours > 0)
            return `${intHours} ${this.UIString(hourString)}, ${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`;
        if (intMinutes > 0)
            return `${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`;
        return `${intSeconds} ${this.UIString(secondString)}`;
    },

    formatTime: function(time)
    {
        if (isNaN(time))
            time = 0;
        var absTime = Math.abs(time);
        var intSeconds = Math.floor(absTime % 60).toFixed(0);
        var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
        var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
        var sign = time < 0 ? '-' : String();

        if (intHours > 0)
            return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);

        return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
    },

    updatePlaying: function()
    {
        this.setPlaying(!this.canPlay());
    },

    setPlaying: function(isPlaying)
    {
        if (!this.video.controls && !this.isFullScreen())
            return;

        if (this.isPlaying === isPlaying)
            return;
        this.isPlaying = isPlaying;

        if (!isPlaying) {
            this.controls.panel.classList.add(this.ClassNames.paused);
            if (this.controls.panelBackground)
                this.controls.panelBackground.classList.add(this.ClassNames.paused);
            this.controls.playButton.classList.add(this.ClassNames.paused);
            this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
            this.showControls();
        } else {
            this.controls.panel.classList.remove(this.ClassNames.paused);
            if (this.controls.panelBackground)
               this.controls.panelBackground.classList.remove(this.ClassNames.paused);
            this.controls.playButton.classList.remove(this.ClassNames.paused);
            this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
            this.resetHideControlsTimer();
            this.canToggleShowControlsButton = true;
        }
    },

    updateForShowingControls: function()
    {
        this.updateLayoutForDisplayedWidth();
        this.setNeedsTimelineMetricsUpdate();
        this.updateTime();
        this.updateProgress();
        this.drawVolumeBackground();
        this.drawTimelineBackground();
        this.controls.panel.classList.add(this.ClassNames.show);
        this.controls.panel.classList.remove(this.ClassNames.hidden);
        if (this.controls.panelBackground) {
            this.controls.panelBackground.classList.add(this.ClassNames.show);
            this.controls.panelBackground.classList.remove(this.ClassNames.hidden);
        }
    },

    showShowControlsButton: function (shouldShow) {
        this.showControlsButton.hidden = !shouldShow;
        if (shouldShow && this.shouldHaveControls())
            this.showControlsButton.focus();
    },

    showControls: function(focusControls)
    {
        this.updateShouldListenForPlaybackTargetAvailabilityEvent();
        if (!this.video.controls && !this.isFullScreen())
            return;

        this.updateForShowingControls();
        if (this.shouldHaveControls() && !this.controls.panel.parentElement) {
            this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
            this.base.appendChild(this.controls.panel);
            if (focusControls)
                this.controls.playButton.focus();
        }
        this.showShowControlsButton(false);
    },

    hideControls: function()
    {
        if (this.controlsAlwaysVisible())
            return;

        this.clearHideControlsTimer();
        this.updateShouldListenForPlaybackTargetAvailabilityEvent();
        this.controls.panel.classList.remove(this.ClassNames.show);
        if (this.controls.panelBackground)
            this.controls.panelBackground.classList.remove(this.ClassNames.show);
        this.showShowControlsButton(this.isPlayable() && this.isPlaying && this.canToggleShowControlsButton);
    },

    setNeedsUpdateForDisplayedWidth: function()
    {
        this.currentDisplayWidth = 0;
    },

    scheduleUpdateLayoutForDisplayedWidth: function()
    {
        setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
    },

    isControlVisible: function(control)
    {
        if (!control)
            return false;
        if (!this.root.contains(control))
            return false;
        return !control.classList.contains(this.ClassNames.hidden)
    },

    updateLayoutForDisplayedWidth: function()
    {
        if (!this.controls || !this.controls.panel)
            return;

        var visibleWidth = this.controls.panel.getBoundingClientRect().width;
        if (this._pageScaleFactor > 1)
            visibleWidth *= this._pageScaleFactor;

        if (visibleWidth <= 0 || visibleWidth == this.currentDisplayWidth)
            return;

        this.currentDisplayWidth = visibleWidth;

        // Filter all the buttons which are not explicitly hidden.
        var buttons = [this.controls.playButton, this.controls.rewindButton, this.controls.captionButton,
                       this.controls.fullscreenButton, this.controls.pictureInPictureButton,
                       this.controls.wirelessTargetPicker, this.controls.muteBox];
        var visibleButtons = buttons.filter(this.isControlVisible, this);

        // This tells us how much room we need in order to display every visible button.
        var visibleButtonWidth = this.ButtonWidth * visibleButtons.length;

        var currentTimeWidth = this.currentTimeClone.getBoundingClientRect().width;
        var remainingTimeWidth = this.remainingTimeClone.getBoundingClientRect().width;

        // Check if there is enough room for the scrubber.
        var shouldDropTimeline = (visibleWidth - visibleButtonWidth - currentTimeWidth - remainingTimeWidth) < this.MinimumTimelineWidth;
        this.controls.timeline.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
        this.controls.currentTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
        this.controls.thumbnailTrack.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
        this.controls.remainingTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);

        // Then controls in the following order:
        var removeOrder = [this.controls.wirelessTargetPicker, this.controls.pictureInPictureButton,
                           this.controls.captionButton, this.controls.muteBox, this.controls.rewindButton,
                           this.controls.fullscreenButton];
        removeOrder.forEach(function(control) {
            var shouldDropControl = visibleWidth < visibleButtonWidth && this.isControlVisible(control);
            control.classList.toggle(this.ClassNames.dropped, shouldDropControl);
            if (shouldDropControl)
                visibleButtonWidth -= this.ButtonWidth;
        }, this);
    },

    controlsAlwaysVisible: function()
    {
        if (this.presentationMode() === 'picture-in-picture')
            return true;

        return this.isAudio() || this.currentPlaybackTargetIsWireless() || this.scrubbing;
    },

    controlsAreHidden: function()
    {
        return !this.controlsAlwaysVisible() && !this.controls.panel.classList.contains(this.ClassNames.show) && !this.controls.panel.parentElement;
    },

    removeControls: function()
    {
        if (this.controls.panel.parentNode)
            this.controls.panel.parentNode.removeChild(this.controls.panel);
        this.destroyCaptionMenu();
    },

    addControls: function()
    {
        this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
        this.base.appendChild(this.controls.panel);
        this.updateControls();
    },

    hasControls: function()
    {
        return this.controls.panel.parentElement;
    },

    updateTime: function()
    {
        var currentTime = this.video.currentTime;
        var timeRemaining = currentTime - this.video.duration;
        this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
        this.controls.currentTime.setAttribute('aria-label', `${this.UIString('Elapsed')} ${this.formatTimeDescription(currentTime)}`);
        this.controls.timeline.value = this.video.currentTime;
        this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
        this.controls.remainingTime.setAttribute('aria-label', `${this.UIString('Remaining')} ${this.formatTimeDescription(timeRemaining)}`);
        
        // Mark the timeline value in percentage format in accessibility.
        var timeElapsedPercent = currentTime / this.video.duration;
        timeElapsedPercent = Math.max(Math.min(1, timeElapsedPercent), 0);
        this.controls.timeline.setAttribute('aria-valuetext', `${parseInt(timeElapsedPercent * 100)}%`);
    },
    
    updateControlsWhileScrubbing: function()
    {
        if (!this.scrubbing)
            return;

        var currentTime = (this.controls.timeline.value / this.controls.timeline.max) * this.video.duration;
        var timeRemaining = currentTime - this.video.duration;
        this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
        this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
        this.drawTimelineBackground();
    },

    updateReadyState: function()
    {
        this.updateStatusDisplay();
    },

    setStatusHidden: function(hidden)
    {
        if (this.statusHidden === hidden)
            return;

        this.statusHidden = hidden;

        if (hidden) {
            this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
            this.controls.currentTime.classList.remove(this.ClassNames.hidden);
            this.controls.timeline.classList.remove(this.ClassNames.hidden);
            this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
            this.setNeedsTimelineMetricsUpdate();
            this.showControls();
        } else {
            this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
            this.controls.currentTime.classList.add(this.ClassNames.hidden);
            this.controls.timeline.classList.add(this.ClassNames.hidden);
            this.controls.remainingTime.classList.add(this.ClassNames.hidden);
            this.hideControls();
        }
        this.updateWirelessTargetAvailable();
    },

    trackHasThumbnails: function(track)
    {
        return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
    },

    updateThumbnail: function()
    {
        for (var i = 0; i < this.video.textTracks.length; ++i) {
            var track = this.video.textTracks[i];
            if (this.trackHasThumbnails(track)) {
                this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
                return;
            }
        }

        this.controls.thumbnail.classList.add(this.ClassNames.hidden);
    },

    updateCaptionButton: function()
    {
        var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
        var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);

        if ((textTracks && textTracks.length) || (audioTracks && audioTracks.length > 1))
            this.controls.captionButton.classList.remove(this.ClassNames.hidden);
        else
            this.controls.captionButton.classList.add(this.ClassNames.hidden);
        this.setNeedsUpdateForDisplayedWidth();
        this.updateLayoutForDisplayedWidth();
    },

    updateCaptionContainer: function()
    {
        if (!this.host.textTrackContainer)
            return;

        var hasClosedCaptions = this.video.webkitHasClosedCaptions;
        var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);

        if (hasClosedCaptions && hasHiddenClass)
            this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
        else if (!hasClosedCaptions && !hasHiddenClass)
            this.host.textTrackContainer.classList.add(this.ClassNames.hidden);

        this.updateBase();
        this.host.updateTextTrackContainer();
    },

    buildCaptionMenu: function()
    {
        var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
        var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);

        if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length))
            return;

        this.captionMenu = document.createElement('div');
        this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
        this.captionMenu.setAttribute('id', 'audioAndTextTrackMenu');
        this.base.appendChild(this.captionMenu);
        this.captionMenuItems = [];

        var offItem = this.host.captionMenuOffItem;
        var automaticItem = this.host.captionMenuAutomaticItem;
        var displayMode = this.host.captionDisplayMode;

        var list = document.createElement('div');
        this.captionMenu.appendChild(list);
        list.classList.add(this.ClassNames.list);

        if (audioTracks && audioTracks.length > 1) {
            var heading = document.createElement('h3');
            heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label
            list.appendChild(heading);
            heading.innerText = this.UIString('Audio');

            var ul = document.createElement('ul');
            ul.setAttribute('role', 'menu');
            ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading');
            list.appendChild(ul);

            for (var i = 0; i < audioTracks.length; ++i) {
                var menuItem = document.createElement('li');
                menuItem.setAttribute('role', 'menuitemradio');
                menuItem.setAttribute('tabindex', '-1');
                this.captionMenuItems.push(menuItem);
                this.listenFor(menuItem, 'click', this.audioTrackItemSelected);
                this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp);
                ul.appendChild(menuItem);

                var track = audioTracks[i];
                menuItem.innerText = this.host.displayNameForTrack(track);
                menuItem.track = track;

                var itemCheckmark = document.createElement("img");
                itemCheckmark.classList.add("checkmark-container");
                menuItem.insertBefore(itemCheckmark, menuItem.firstChild);

                if (track.enabled) {
                    menuItem.classList.add(this.ClassNames.selected);
                    menuItem.setAttribute('tabindex', '0');
                    menuItem.setAttribute('aria-checked', 'true');
                }
            }
        }

        if (textTracks && textTracks.length > 2) {
            var heading = document.createElement('h3');
            heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
            list.appendChild(heading);
            heading.innerText = this.UIString('Subtitles');

            var ul = document.createElement('ul');
            ul.setAttribute('role', 'menu');
            ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
            list.appendChild(ul);

            for (var i = 0; i < textTracks.length; ++i) {
                var menuItem = document.createElement('li');
                menuItem.setAttribute('role', 'menuitemradio');
                menuItem.setAttribute('tabindex', '-1');
                this.captionMenuItems.push(menuItem);
                this.listenFor(menuItem, 'click', this.captionItemSelected);
                this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
                ul.appendChild(menuItem);

                var track = textTracks[i];
                menuItem.innerText = this.host.displayNameForTrack(track);
                menuItem.track = track;

                var itemCheckmark = document.createElement("img");
                itemCheckmark.classList.add("checkmark-container");
                menuItem.insertBefore(itemCheckmark, menuItem.firstChild);

                if (track === offItem) {
                    var offMenu = menuItem;
                    continue;
                }

                if (track === automaticItem) {
                    if (displayMode === 'automatic') {
                        menuItem.classList.add(this.ClassNames.selected);
                        menuItem.setAttribute('tabindex', '0');
                        menuItem.setAttribute('aria-checked', 'true');
                    }
                    continue;
                }

                if (displayMode != 'automatic' && track.mode === 'showing') {
                    var trackMenuItemSelected = true;
                    menuItem.classList.add(this.ClassNames.selected);
                    menuItem.setAttribute('tabindex', '0');
                    menuItem.setAttribute('aria-checked', 'true');
                }

            }

            if (offMenu && (displayMode === 'forced-only' || displayMode === 'manual') && !trackMenuItemSelected) {
                offMenu.classList.add(this.ClassNames.selected);
                offMenu.setAttribute('tabindex', '0');
                offMenu.setAttribute('aria-checked', 'true');
            }
        }
        
        // focus first selected menuitem
        for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
            var item = this.captionMenuItems[i];
            if (item.classList.contains(this.ClassNames.selected)) {
                item.focus();
                break;
            }
        }
        
    },

    captionItemSelected: function(event)
    {
        this.host.setSelectedTextTrack(event.target.track);
        this.destroyCaptionMenu();
    },

    focusSiblingCaptionItem: function(event)
    {
        var currentItem = event.target;
        var pendingItem = false;
        switch(event.keyCode) {
        case this.KeyCodes.left:
        case this.KeyCodes.up:
            pendingItem = currentItem.previousSibling;
            break;
        case this.KeyCodes.right:
        case this.KeyCodes.down:
            pendingItem = currentItem.nextSibling;
            break;
        }
        if (pendingItem) {
            currentItem.setAttribute('tabindex', '-1');
            pendingItem.setAttribute('tabindex', '0');
            pendingItem.focus();
        }
    },

    handleCaptionItemKeyUp: function(event)
    {
        switch (event.keyCode) {
        case this.KeyCodes.enter:
        case this.KeyCodes.space:
            this.captionItemSelected(event);
            break;
        case this.KeyCodes.escape:
            this.destroyCaptionMenu();
            break;
        case this.KeyCodes.left:
        case this.KeyCodes.up:
        case this.KeyCodes.right:
        case this.KeyCodes.down:
            this.focusSiblingCaptionItem(event);
            break;
        default:
            return;
        }
        // handled
        event.stopPropagation();
        event.preventDefault();
    },

    audioTrackItemSelected: function(event)
    {
        for (var i = 0; i < this.video.audioTracks.length; ++i) {
            var track = this.video.audioTracks[i];
            track.enabled = (track == event.target.track);
        }

        this.destroyCaptionMenu();
    },

    focusSiblingAudioTrackItem: function(event)
    {
        var currentItem = event.target;
        var pendingItem = false;
        switch(event.keyCode) {
            case this.KeyCodes.left:
            case this.KeyCodes.up:
                pendingItem = currentItem.previousSibling;
                break;
            case this.KeyCodes.right:
            case this.KeyCodes.down:
                pendingItem = currentItem.nextSibling;
                break;
        }
        if (pendingItem) {
            currentItem.setAttribute('tabindex', '-1');
            pendingItem.setAttribute('tabindex', '0');
            pendingItem.focus();
        }
    },

    handleAudioTrackItemKeyUp: function(event)
    {
        switch (event.keyCode) {
            case this.KeyCodes.enter:
            case this.KeyCodes.space:
                this.audioTrackItemSelected(event);
                break;
            case this.KeyCodes.escape:
                this.destroyCaptionMenu();
                break;
            case this.KeyCodes.left:
            case this.KeyCodes.up:
            case this.KeyCodes.right:
            case this.KeyCodes.down:
                this.focusSiblingAudioTrackItem(event);
                break;
            default:
                return;
        }
        // handled
        event.stopPropagation();
        event.preventDefault();
    },

    destroyCaptionMenu: function()
    {
        if (!this.captionMenu)
            return;

        this.captionMenuItems.forEach(function(item){
            this.stopListeningFor(item, 'click', this.captionItemSelected);
            this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
        }, this);

        // FKA and AX: focus the trigger before destroying the element with focus
        if (this.controls.captionButton)
            this.controls.captionButton.focus();

        if (this.captionMenu.parentNode)
            this.captionMenu.parentNode.removeChild(this.captionMenu);
        delete this.captionMenu;
        delete this.captionMenuItems;
    },

    updateHasAudio: function()
    {
        if (this.video.audioTracks.length && !this.currentPlaybackTargetIsWireless())
            this.controls.muteBox.classList.remove(this.ClassNames.hidden);
        else
            this.controls.muteBox.classList.add(this.ClassNames.hidden);

        this.setNeedsUpdateForDisplayedWidth();
        this.updateLayoutForDisplayedWidth();
    },

    updateHasVideo: function()
    {
        this.controls.panel.classList.toggle(this.ClassNames.noVideo, !this.hasVideo());
        // The availability of the picture-in-picture button as well as the full-screen
        // button depends no the value returned by hasVideo(), so make sure we invalidate
        // the availability of both controls.
        this.updateFullscreenButtons();
    },

    updateVolume: function()
    {
        if (this.video.muted || !this.video.volume) {
            this.controls.muteButton.classList.add(this.ClassNames.muted);
            this.controls.volume.value = 0;
        } else {
            this.controls.muteButton.classList.remove(this.ClassNames.muted);
            this.controls.volume.value = this.video.volume;
        }
        this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
        this.drawVolumeBackground();
    },

    isAudio: function()
    {
        return this.video instanceof HTMLAudioElement;
    },

    clearHideControlsTimer: function()
    {
        if (this.hideTimer)
            clearTimeout(this.hideTimer);
        this.hideTimer = null;
    },

    resetHideControlsTimer: function()
    {
        if (this.hideTimer) {
            clearTimeout(this.hideTimer);
            this.hideTimer = null;
        }

        if (this.isPlaying)
            this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
    },

    handlePictureInPictureButtonClicked: function(event) {
        if (!('webkitSetPresentationMode' in this.video))
            return;

        if (this.presentationMode() === 'picture-in-picture')
            this.video.webkitSetPresentationMode('inline');
        else
            this.video.webkitSetPresentationMode('picture-in-picture');
    },

    currentPlaybackTargetIsWireless: function() {
        if (Controller.gSimulateWirelessPlaybackTarget)
            return true;

        if (!this.currentTargetIsWireless || this.wirelessPlaybackDisabled)
            return false;

        return true;
    },

    updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
        var shouldListen = true;
        if (this.video.error)
            shouldListen = false;
        if (!this.isAudio() && !this.video.paused && this.controlsAreHidden())
            shouldListen = false;
        if (document.hidden)
            shouldListen = false;

        this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen);
    },

    updateWirelessPlaybackStatus: function() {
        if (this.currentPlaybackTargetIsWireless()) {
            var deviceName = "";
            var deviceType = "";
            var type = this.host.externalDeviceType;
            if (type == "airplay") {
                deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
                deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
            } else if (type == "tvout") {
                deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
                deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
            }

            this.controls.inlinePlaybackPlaceholderTextTop.innerText = deviceType;
            this.controls.inlinePlaybackPlaceholderTextBottom.innerText = deviceName;
            this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName);
            this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.appleTV);
            this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
            this.controls.wirelessTargetPicker.classList.add(this.ClassNames.playing);
            if (!this.isFullScreen() && (this.video.offsetWidth <= 250 || this.video.offsetHeight <= 200)) {
                this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.small);
                this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.small);
                this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.small);
            } else {
                this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.small);
                this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.small);
                this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.small);
            }
            this.controls.volumeBox.classList.add(this.ClassNames.hidden);
            this.controls.muteBox.classList.add(this.ClassNames.hidden);
            this.updateBase();
            this.showControls();
        } else {
            this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
            this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.appleTV);
            this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.playing);
            this.controls.volumeBox.classList.remove(this.ClassNames.hidden);
            this.controls.muteBox.classList.remove(this.ClassNames.hidden);
        }
        this.setNeedsUpdateForDisplayedWidth();
        this.updateLayoutForDisplayedWidth();
        this.reconnectControls();
        this.updateWirelessTargetPickerButton();
    },

    updateWirelessTargetAvailable: function() {
        this.currentTargetIsWireless = this.video.webkitCurrentPlaybackTargetIsWireless;
        this.wirelessPlaybackDisabled = this.video.webkitWirelessVideoPlaybackDisabled;

        var wirelessPlaybackTargetsAvailable = Controller.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets;
        if (this.wirelessPlaybackDisabled)
            wirelessPlaybackTargetsAvailable = false;

        if (wirelessPlaybackTargetsAvailable && this.isPlayable())
            this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
        else
            this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
        this.setNeedsUpdateForDisplayedWidth();
        this.updateLayoutForDisplayedWidth();
    },

    handleWirelessPickerButtonClicked: function(event)
    {
        this.video.webkitShowPlaybackTargetPicker();
        return true;
    },

    handleWirelessPlaybackChange: function(event) {
        this.updateWirelessTargetAvailable();
        this.updateWirelessPlaybackStatus();
        this.setNeedsTimelineMetricsUpdate();
    },

    handleWirelessTargetAvailableChange: function(event) {
        var wirelessPlaybackTargetsAvailable = event.availability == "available";
        if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
            return;

        this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
        this.updateWirelessTargetAvailable();
        this.setNeedsTimelineMetricsUpdate();
    },

    setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) {
        if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen)
            return;

        if (shouldListen && this.video.error)
            return;

        this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen;
        if (shouldListen)
            this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
        else
            this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
    },

    get scrubbing()
    {
        return this._scrubbing;
    },

    set scrubbing(flag)
    {
        if (this._scrubbing == flag)
            return;
        this._scrubbing = flag;

        if (this._scrubbing)
            this.wasPlayingWhenScrubbingStarted = !this.video.paused;
        else if (this.wasPlayingWhenScrubbingStarted && this.video.paused) {
            this.video.play();
            this.resetHideControlsTimer();
        }
    },

    get pageScaleFactor()
    {
        return this._pageScaleFactor;
    },

    set pageScaleFactor(newScaleFactor)
    {
        if (this._pageScaleFactor === newScaleFactor)
            return;

        this._pageScaleFactor = newScaleFactor;
    },

    set usesLTRUserInterfaceLayoutDirection(usesLTRUserInterfaceLayoutDirection)
    {
        this.controls.volumeBox.classList.toggle(this.ClassNames.usesLTRUserInterfaceLayoutDirection, usesLTRUserInterfaceLayoutDirection);
    },

    handleRootResize: function(event)
    {
        this.updateLayoutForDisplayedWidth();
        this.setNeedsTimelineMetricsUpdate();
        this.updateTimelineMetricsIfNeeded();
        this.drawTimelineBackground();
    },

    getCurrentControlsStatus: function ()
    {
        var result = {
            idiom: this.idiom,
            status: "ok"
        };

        var elements = [
            {
                name: "Show Controls",
                object: this.showControlsButton,
                extraProperties: ["hidden"],
            },
            {
                name: "Status Display",
                object: this.controls.statusDisplay,
                styleValues: ["display"],
                extraProperties: ["textContent"],
            },
            {
                name: "Play Button",
                object: this.controls.playButton,
                extraProperties: ["hidden"],
            },
            {
                name: "Rewind Button",
                object: this.controls.rewindButton,
                extraProperties: ["hidden"],
            },
            {
                name: "Timeline Box",
                object: this.controls.timelineBox,
            },
            {
                name: "Mute Box",
                object: this.controls.muteBox,
                extraProperties: ["hidden"],
            },
            {
                name: "Fullscreen Button",
                object: this.controls.fullscreenButton,
                extraProperties: ["hidden"],
            },
            {
                name: "AppleTV Device Picker",
                object: this.controls.wirelessTargetPicker,
                styleValues: ["display"],
                extraProperties: ["hidden"],
            },
            {
                name: "Picture-in-picture Button",
                object: this.controls.pictureInPictureButton,
                extraProperties: ["parentElement", "hidden"],
            },
            {
                name: "Caption Button",
                object: this.controls.captionButton,
                extraProperties: ["hidden"],
            },
            {
                name: "Timeline",
                object: this.controls.timeline,
                extraProperties: ["hidden"],
            },
            {
                name: "Current Time",
                object: this.controls.currentTime,
                extraProperties: ["hidden"],
            },
            {
                name: "Thumbnail Track",
                object: this.controls.thumbnailTrack,
                extraProperties: ["hidden"],
            },
            {
                name: "Time Remaining",
                object: this.controls.remainingTime,
                extraProperties: ["hidden"],
            },
            {
                name: "Track Menu",
                object: this.captionMenu,
            },
            {
                name: "Inline playback placeholder",
                object: this.controls.inlinePlaybackPlaceholder,
            },
            {
                name: "Media Controls Panel",
                object: this.controls.panel,
                extraProperties: ["hidden"],
            },
            {
                name: "Control Base Element",
                object: this.base || null,
            },
        ];

        elements.forEach(function (element) {
            var obj = element.object;
            delete element.object;

            element.computedStyle = {};
            if (obj && element.styleValues) {
                var computedStyle = window.getComputedStyle(obj);
                element.styleValues.forEach(function (propertyName) {
                    element.computedStyle[propertyName] = computedStyle[propertyName];
                });
                delete element.styleValues;
            }

            element.bounds = obj ? obj.getBoundingClientRect() : null;
            element.className = obj ? obj.className : null;
            element.ariaLabel = obj ? obj.getAttribute('aria-label') : null;

            if (element.extraProperties) {
                element.extraProperties.forEach(function (property) {
                    element[property] = obj ? obj[property] : null;
                });
                delete element.extraProperties;
            }

             element.element = obj;
        });

        result.elements = elements;

        return JSON.stringify(result);
    }

};