mediaControlsiOS.js   [plain text]


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

function ControllerIOS(root, video, host)
{
    this.doingSetup = true;
    this._pageScaleFactor = 1;

    this.timelineContextName = "_webkit-media-controls-timeline-" + host.generateUUID();

    Controller.call(this, root, video, host);

    this.setNeedsTimelineMetricsUpdate();

    this._timelineIsHidden = false;
    this._currentDisplayWidth = 0;
    this.scheduleUpdateLayoutForDisplayedWidth();

    host.controlsDependOnPageScaleFactor = true;
    this.doingSetup = false;
};

/* Enums */
ControllerIOS.StartPlaybackControls = 2;

ControllerIOS.prototype = {
    /* Constants */
    MinimumTimelineWidth: 150,
    ButtonWidth: 42,

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

    createBase: function() {
        Controller.prototype.createBase.call(this);

        var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('div');
        startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button');
        startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback'));
        startPlaybackButton.setAttribute('role', 'button');

        var startPlaybackBackground = document.createElement('div');
        startPlaybackBackground.setAttribute('pseudo', '-webkit-media-controls-start-playback-background');
        startPlaybackBackground.classList.add('webkit-media-controls-start-playback-background');
        startPlaybackButton.appendChild(startPlaybackBackground);

        var startPlaybackGlyph = document.createElement('div');
        startPlaybackGlyph.setAttribute('pseudo', '-webkit-media-controls-start-playback-glyph');
        startPlaybackGlyph.classList.add('webkit-media-controls-start-playback-glyph');
        startPlaybackButton.appendChild(startPlaybackGlyph);

        this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart);
        this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange);
        this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd);
        this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart);
        this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove);
        this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut);

        this.listenFor(document, 'visibilitychange', this.handleVisibilityChange);
    },

    shouldHaveStartPlaybackButton: function() {
        var allowsInline = this.host.allowsInlineMediaPlayback;

        if (this.isPlaying || (this.hasPlayed && allowsInline))
            return false;

        if (this.isAudio() && allowsInline)
            return false;

        if (this.doingSetup)
            return true;

        if (this.isFullScreen())
            return false;

        if (!this.video.currentSrc && this.video.error)
            return false;

        if (!this.video.controls && allowsInline)
            return false;

        if (this.video.currentSrc && this.video.error)
            return true;

        return true;
    },

    shouldHaveControls: function() {
        if (this.shouldHaveStartPlaybackButton())
            return false;

        return Controller.prototype.shouldHaveControls.call(this);
    },

    shouldHaveAnyUI: function() {
        return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless();
    },

    createControls: function() {
        Controller.prototype.createControls.call(this);

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

        var wirelessTargetPicker = this.controls.wirelessTargetPicker;
        this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart);
        this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd);
        this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel);

        this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart);
        this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd);
        this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel);

        this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart);
        this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd);
        this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel);
        this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart);
        this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd);
        this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel);
        this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart);
        this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd);
        this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel);
        this.listenFor(this.controls.pictureInPictureButton, 'touchstart', this.handlePictureInPictureTouchStart);
        this.listenFor(this.controls.pictureInPictureButton, 'touchend', this.handlePictureInPictureTouchEnd);
        this.listenFor(this.controls.pictureInPictureButton, 'touchcancel', this.handlePictureInPictureTouchCancel);
        this.listenFor(this.controls.timeline, 'touchstart', this.handleTimelineTouchStart);
        this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked);

        this.controls.timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
    },

    setControlsType: function(type) {
        if (type === this.controlsType)
            return;
        Controller.prototype.setControlsType.call(this, type);

        if (type === ControllerIOS.StartPlaybackControls)
            this.addStartPlaybackControls();
        else
            this.removeStartPlaybackControls();
    },

    addStartPlaybackControls: function() {
        this.base.appendChild(this.controls.startPlaybackButton);
        this.showShowControlsButton(false);
    },

    removeStartPlaybackControls: function() {
        if (this.controls.startPlaybackButton.parentNode)
            this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton);
    },

    reconnectControls: function()
    {
        Controller.prototype.reconnectControls.call(this);

        if (this.controlsType === ControllerIOS.StartPlaybackControls)
            this.addStartPlaybackControls();
    },

    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.playButton);
        this.controls.panel.appendChild(this.controls.statusDisplay);
        this.controls.panel.appendChild(this.controls.timelineBox);
        this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
        if (!this.isLive) {
            this.controls.timelineBox.appendChild(this.controls.currentTime);
            this.controls.timelineBox.appendChild(this.controls.timeline);
            this.controls.timelineBox.appendChild(this.controls.remainingTime);
        }
        if (this.isAudio()) {
            // Hide the scrubber on audio until the user starts playing.
            this.controls.timelineBox.classList.add(this.ClassNames.hidden);
        } else {
            this.updatePictureInPictureButton();
            this.controls.panel.appendChild(this.controls.fullscreenButton);
        }
    },

    configureFullScreenControls: function() {
        // Explicitly do nothing to override base-class behavior.
    },

    controlsAreHidden: function()
    {
        // Controls are only ever actually hidden when they are removed from the tree
        return !this.controls.panelContainer.parentElement;
    },

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

    updateControls: function() {
        if (this.shouldHaveStartPlaybackButton())
            this.setControlsType(ControllerIOS.StartPlaybackControls);
        else if (this.presentationMode() === "fullscreen")
            this.setControlsType(Controller.FullScreenControls);
        else
            this.setControlsType(Controller.InlineControls);

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

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

        if (!width || !height)
            return;

        var played = this.video.currentTime / this.video.duration;
        var buffered = 0;
        var bufferedRanges = this.video.buffered;
        if (bufferedRanges && bufferedRanges.length)
            buffered = Math.max(bufferedRanges.end(bufferedRanges.length - 1), buffered);

        buffered /= this.video.duration;
        buffered = Math.max(buffered, played);

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

        ctx.clearRect(0, 0, width, height);

        var midY = height / 2;

        // 1. Draw the buffered part and played parts, using
        // solid rectangles that are clipped to the outside of
        // the lozenge.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
        ctx.closePath();
        ctx.clip();
        ctx.fillStyle = "white";
        ctx.fillRect(0, 0, Math.round(width * played) + 2, height);
        ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
        ctx.fillRect(Math.round(width * played) + 2, 0, Math.round(width * (buffered - played)) + 2, height);
        ctx.restore();

        // 2. Draw the outline with a clip path that subtracts the
        // middle of a lozenge. This produces a better result than
        // stroking.
        ctx.save();
        ctx.beginPath();
        this.addRoundedRect(ctx, 1, midY - 3, width - 2, 6, 3);
        this.addRoundedRect(ctx, 2, midY - 2, width - 4, 4, 2);
        ctx.closePath();
        ctx.clip("evenodd");
        ctx.fillStyle = "rgba(0, 0, 0, 0.55)";
        ctx.fillRect(Math.round(width * buffered) + 2, 0, width, height);
        ctx.restore();
    },

    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('0' + intMinutes).slice(-2) + ":" + String('0' + intSeconds).slice(-2);

        return sign + String('0' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('0' + intSeconds).slice(-2);
    },

    handlePlayButtonTouchStart: function() {
        this.controls.playButton.classList.add('active');
    },

    handlePlayButtonTouchEnd: function(event) {
        this.controls.playButton.classList.remove('active');

        if (this.canPlay()) {
            this.video.play();
            this.showControls();
        } else
            this.video.pause();

        return true;
    },

    handlePlayButtonTouchCancel: function(event) {
        this.controls.playButton.classList.remove('active');
        return true;
    },

    handleBaseGestureStart: function(event) {
        this.gestureStartTime = new Date();
        // If this gesture started with two fingers inside the video, then
        // don't treat it as a potential zoom, unless we're still waiting
        // to play.
        if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls)
            event.preventDefault();
    },

    handleBaseGestureChange: function(event) {
        if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls)
            return;

        var scaleDetectionThreshold = 0.2;
        if (event.scale > 1 + scaleDetectionThreshold || event.scale < 1 - scaleDetectionThreshold)
            delete this.lastDoubleTouchTime;

        if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0)
            event.preventDefault();

        var currentGestureTime = new Date();
        var duration = (currentGestureTime - this.gestureStartTime) / 1000;
        if (!duration)
            return;

        var velocity = Math.abs(event.scale - 1) / duration;

        var pinchOutVelocityThreshold = 2;
        var pinchOutGestureScaleThreshold = 1.25;
        if (velocity < pinchOutVelocityThreshold || event.scale < pinchOutGestureScaleThreshold)
            return;

        delete this.gestureStartTime;
        this.video.webkitEnterFullscreen();
    },

    handleBaseGestureEnd: function(event) {
        delete this.gestureStartTime;
    },

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

        this.mostRecentNumberOfTargettedTouches = event.targetTouches.length;

        if (this.controlsAreHidden() || !this.controls.panel.classList.contains(this.ClassNames.show)) {
            this.showControls();
            this.resetHideControlsTimer();
        } else if (!this.canPlay())
            this.hideControls();
    },

    handlePanelTouchStart: function(event) {
        this.video.style.webkitUserSelect = 'none';
    },

    handlePanelTouchEnd: function(event) {
        this.video.style.removeProperty('-webkit-user-select');
    },

    handlePanelTouchCancel: function(event) {
        this.video.style.removeProperty('-webkit-user-select');
    },

    handleVisibilityChange: function(event) {
        this.updateShouldListenForPlaybackTargetAvailabilityEvent();
    },

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

    handleFullscreenButtonClicked: function(event) {
        if ('webkitSetPresentationMode' in this.video) {
            if (this.presentationMode() === 'fullscreen')
                this.video.webkitSetPresentationMode('inline');
            else
                this.video.webkitSetPresentationMode('fullscreen');

            return;
        }

        if (this.isFullScreen())
            this.video.webkitExitFullscreen();
        else
            this.video.webkitEnterFullscreen();
    },

    handleFullscreenTouchStart: function() {
        this.controls.fullscreenButton.classList.add('active');
    },

    handleFullscreenTouchEnd: function(event) {
        this.controls.fullscreenButton.classList.remove('active');

        this.handleFullscreenButtonClicked();

        return true;
    },

    handleFullscreenTouchCancel: function(event) {
        this.controls.fullscreenButton.classList.remove('active');
        return true;
    },

    handlePictureInPictureTouchStart: function() {
        this.controls.pictureInPictureButton.classList.add('active');
    },

    handlePictureInPictureTouchEnd: function(event) {
        this.controls.pictureInPictureButton.classList.remove('active');

        this.handlePictureInPictureButtonClicked();

        return true;
    },

    handlePictureInPictureTouchCancel: function(event) {
        this.controls.pictureInPictureButton.classList.remove('active');
        return true;
    },

    handleStartPlaybackButtonTouchStart: function(event) {
        this.controls.startPlaybackButton.classList.add('active');
        this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-glyph').classList.add('active');
    },

    handleStartPlaybackButtonTouchEnd: function(event) {
        this.controls.startPlaybackButton.classList.remove('active');
        this.controls.startPlaybackButton.querySelector('.webkit-media-controls-start-playback-glyph').classList.remove('active');

        if (this.video.error)
            return true;

        this.video.play();
        this.canToggleShowControlsButton = true;
        this.updateControls();

        return true;
    },

    handleStartPlaybackButtonTouchCancel: function(event) {
        this.controls.startPlaybackButton.classList.remove('active');
        return true;
    },

    handleTimelineTouchStart: function(event) {
        this.scrubbing = true;
        this.listenFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
        this.listenFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
    },

    handleTimelineTouchEnd: function(event) {
        this.stopListeningFor(this.controls.timeline, 'touchend', this.handleTimelineTouchEnd);
        this.stopListeningFor(this.controls.timeline, 'touchcancel', this.handleTimelineTouchEnd);
        this.scrubbing = false;
    },

    handleWirelessPickerButtonTouchStart: function() {
        if (!this.video.error)
            this.controls.wirelessTargetPicker.classList.add('active');
    },

    handleWirelessPickerButtonTouchEnd: function(event) {
        this.controls.wirelessTargetPicker.classList.remove('active');
        return this.handleWirelessPickerButtonClicked();
    },

    handleWirelessPickerButtonTouchCancel: function(event) {
        this.controls.wirelessTargetPicker.classList.remove('active');
        return true;
    },

    updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
        if (this.controlsType === ControllerIOS.StartPlaybackControls) {
            this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
            return;
        }

        Controller.prototype.updateShouldListenForPlaybackTargetAvailabilityEvent.call(this);
    },

    updateWirelessTargetPickerButton: function() {
    },

    updateStatusDisplay: function(event)
    {
        this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null);
        this.controls.startPlaybackButton.querySelector(".webkit-media-controls-start-playback-glyph").classList.toggle(this.ClassNames.failed, this.video.error !== null);
        Controller.prototype.updateStatusDisplay.call(this, event);
    },

    setPlaying: function(isPlaying)
    {
        Controller.prototype.setPlaying.call(this, isPlaying);

        this.updateControls();

        if (isPlaying && this.isAudio())
            this.controls.timelineBox.classList.remove(this.ClassNames.hidden);

        if (isPlaying)
            this.hasPlayed = true;
        else
            this.showControls();
    },

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

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

    setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen)
    {
        if (shouldListen && (this.shouldHaveStartPlaybackButton() || this.video.error))
            return;

        Controller.prototype.setShouldListenForPlaybackTargetAvailabilityEvent.call(this, shouldListen);
    },

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

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

        switch (presentationMode) {
            case 'inline':
                this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture);
                break;
            case 'picture-in-picture':
                this.controls.panelContainer.classList.add(this.ClassNames.pictureInPicture);
                break;
            default:
                this.controls.panelContainer.classList.remove(this.ClassNames.pictureInPicture);
                break;
        }

        Controller.prototype.updatePictureInPicturePlaceholder.call(this, event);
    },

    // Due to the bad way we are faking inheritance here, in particular the extends method
    // on Controller.prototype, we don't copy getters and setters from the prototype. This
    // means we have to implement them again, here in the subclass.
    // FIXME: Use ES6 classes!

    get scrubbing()
    {
        return Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").get.call(this);
    },

    set scrubbing(flag)
    {
        Object.getOwnPropertyDescriptor(Controller.prototype, "scrubbing").set.call(this, flag);
    },

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

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

        this._pageScaleFactor = newScaleFactor;

        var scaleValue = 1 / newScaleFactor;
        var scaleTransform = "scale(" + scaleValue + ")";

        function applyScaleFactorToElement(element) {
            if (scaleValue > 1) {
                element.style.zoom = scaleValue;
                element.style.webkitTransform = "scale(1)";
            } else {
                element.style.zoom = 1;
                element.style.webkitTransform = scaleTransform;
            }
        }

        if (this.controls.startPlaybackButton)
            applyScaleFactorToElement(this.controls.startPlaybackButton);
        if (this.controls.panel) {
            applyScaleFactorToElement(this.controls.panel);
            if (scaleValue > 1) {
                this.controls.panel.style.width = "100%";
                this.controls.timelineBox.style.webkitTextSizeAdjust = (100 * scaleValue) + "%";
            } else {
                var bottomAligment = -2 * scaleValue;
                this.controls.panel.style.bottom = bottomAligment + "px";
                this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px";
                this.controls.panel.style.width = Math.round(newScaleFactor * 100) + "%";
                this.controls.timelineBox.style.webkitTextSizeAdjust = "auto";
            }
            this.controls.panelBackground.style.height = (50 * scaleValue) + "px";

            this.setNeedsTimelineMetricsUpdate();
            this.updateProgress();
            this.scheduleUpdateLayoutForDisplayedWidth();
        }
    },

};

Object.create(Controller.prototype).extend(ControllerIOS.prototype);
Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS });