Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/content/widgets/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 114 kB image not shown  

SSL videocontrols.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

// This is a UA widget. It runs in per-origin UA widget scope,
// to be loaded by UAWidgetsChild.sys.mjs.

/*
 * This is the class of entry. It will construct the actual implementation
 * according to the value of the "controls" property.
 */

this.VideoControlsWidget = class {
  constructor(shadowRoot, prefs) {
    this.shadowRoot = shadowRoot;
    this.prefs = prefs;
    this.element = shadowRoot.host;
    this.document = this.element.ownerDocument;
    this.window = this.document.defaultView;

    this.isMobile = this.window.navigator.appVersion.includes("Android");
  }

  /*
   * Callback called by UAWidgets right after constructor.
   */

  onsetup() {
    this.switchImpl();
  }

  /*
   * Callback called by UAWidgets when the "controls" property changes.
   */

  onchange() {
    this.switchImpl();
  }

  /*
   * Actually switch the implementation.
   * - With "controls" set, the VideoControlsImplWidget controls should load.
   * - Without it, on mobile, the NoControlsMobileImplWidget should load, so
   *   the user could see the click-to-play button when the video/audio is blocked.
   * - Without it, on desktop, the NoControlsPictureInPictureImpleWidget should load
   *   if the video is being viewed in Picture-in-Picture.
   */

  switchImpl() {
    let newImpl;
    let pageURI = this.document.documentURI;
    if (this.element.controls) {
      newImpl = VideoControlsImplWidget;
    } else if (this.isMobile) {
      newImpl = NoControlsMobileImplWidget;
    } else if (VideoControlsWidget.isPictureInPictureVideo(this.element)) {
      newImpl = NoControlsPictureInPictureImplWidget;
    } else if (
      pageURI.startsWith("http://") ||
      pageURI.startsWith("https://")
    ) {
      newImpl = NoControlsDesktopImplWidget;
    }

    // Skip if we are asked to load the same implementation, and
    // the underlying element state hasn't changed in ways that we
    // care about. This can happen if the property is set again
    // without a value change.
    if (this.impl && this.impl.constructor == newImpl) {
      this.impl.onchange();
      return;
    }
    if (this.impl) {
      this.impl.teardown();
      this.shadowRoot.firstChild.remove();
    }
    if (newImpl) {
      this.impl = new newImpl(this.shadowRoot, this.prefs);

      this.mDirection = "ltr";
      let intlUtils = this.window.intlUtils;
      if (intlUtils) {
        this.mDirection = intlUtils.isAppLocaleRTL() ? "rtl" : "ltr";
      }

      this.impl.onsetup(this.mDirection);
    } else {
      this.impl = undefined;
    }
  }

  teardown() {
    if (!this.impl) {
      return;
    }
    this.impl.teardown();
    this.shadowRoot.firstChild.remove();
    delete this.impl;
  }

  onPrefChange(prefName, prefValue) {
    this.prefs[prefName] = prefValue;

    if (!this.impl) {
      return;
    }

    this.impl.onPrefChange(prefName, prefValue);
  }

  // If you change this, also change SEEK_TIME_SECS in PictureInPictureChild.sys.mjs
  static SEEK_TIME_SECS = 5;

  static isPictureInPictureVideo(someVideo) {
    return someVideo.isCloningElementVisually;
  }

  /**
   * Returns true if a <video> meets the requirements to show the Picture-in-Picture
   * toggle. Those requirements currently are:
   *
   * 1. The video must be 45 seconds in length or longer.
   * 2. Neither the width or the height of the video can be less than 140px.
   * 3. The video must have audio.
   * 4. The video must not a MediaStream video (Bug 1592539)
   *
   * This can be overridden via the
   * media.videocontrols.picture-in-picture.video-toggle.always-show pref, which
   * is mostly used for testing.
   *
   * @param {Object} prefs
   *   The preferences set that was passed to the UAWidget.
   * @param {Element} someVideo
   *   The <video> to test.
   * @param {Object} reflowedDimensions
   *   An object representing the reflowed dimensions of the <video>. Properties
   *   are:
   *
   *     videoWidth (Number):
   *       The width of the video in pixels.
   *
   *     videoHeight (Number):
   *       The height of the video in pixels.
   *
   * @return {Boolean}
   */

  static shouldShowPictureInPictureToggle(
    prefs,
    someVideo,
    reflowedDimensions
  ) {
    if (
      prefs[
        "media.videocontrols.picture-in-picture.respect-disablePictureInPicture"
      ] &&
      someVideo.disablePictureInPicture
    ) {
      return false;
    }

    if (isNaN(someVideo.duration)) {
      return false;
    }

    if (
      prefs["media.videocontrols.picture-in-picture.video-toggle.always-show"]
    ) {
      return true;
    }

    const MIN_VIDEO_LENGTH =
      prefs[
        "media.videocontrols.picture-in-picture.video-toggle.min-video-secs"
      ];

    if (someVideo.duration < MIN_VIDEO_LENGTH) {
      return false;
    }

    const MIN_VIDEO_DIMENSION = 140; // pixels
    if (
      reflowedDimensions.videoWidth < MIN_VIDEO_DIMENSION ||
      reflowedDimensions.videoHeight < MIN_VIDEO_DIMENSION
    ) {
      return false;
    }

    return true;
  }

  /**
   * Some variations on the Picture-in-Picture toggle are being experimented with.
   * These variations have slightly different setup parameters from the currently
   * shipping toggle, so this method sets up the experimental toggles in the event
   * that they're being used. It also will enable the appropriate stylesheet for
   * the preferred toggle experiment.
   *
   * @param {Object} prefs
   *   The preferences set that was passed to the UAWidget.
   * @param {ShadowRoot} shadowRoot
   *   The shadowRoot of the <video> element where the video controls are.
   * @param {Element} toggle
   *   The toggle element.
   * @param {Object} reflowedDimensions
   *   An object representing the reflowed dimensions of the <video>. Properties
   *   are:
   *
   *     videoWidth (Number):
   *       The width of the video in pixels.
   *
   *     videoHeight (Number):
   *       The height of the video in pixels.
   */

  static setupToggle(prefs, toggle, reflowedDimensions) {
    // These thresholds are all in pixels
    const SMALL_VIDEO_WIDTH_MAX = 320;
    const MEDIUM_VIDEO_WIDTH_MAX = 720;

    let isSmall = reflowedDimensions.videoWidth <= SMALL_VIDEO_WIDTH_MAX;
    toggle.toggleAttribute("small-video", isSmall);
    toggle.toggleAttribute(
      "medium-video",
      !isSmall && reflowedDimensions.videoWidth <= MEDIUM_VIDEO_WIDTH_MAX
    );

    toggle.setAttribute(
      "position",
      prefs["media.videocontrols.picture-in-picture.video-toggle.position"]
    );
    toggle.toggleAttribute(
      "has-used",
      prefs["media.videocontrols.picture-in-picture.video-toggle.has-used"]
    );
  }
};

this.VideoControlsImplWidget = class {
  constructor(shadowRoot, prefs) {
    this.shadowRoot = shadowRoot;
    this.prefs = prefs;
    this.element = shadowRoot.host;
    this.document = this.element.ownerDocument;
    this.window = this.document.defaultView;
  }

  onsetup(direction) {
    this.generateContent();

    this.shadowRoot.firstElementChild.setAttribute("localedir", direction);

    this.Utils = {
      debug: false,
      video: null,
      videocontrols: null,
      controlBar: null,
      playButton: null,
      muteButton: null,
      volumeControl: null,
      durationLabel: null,
      positionLabel: null,
      scrubber: null,
      progressBar: null,
      bufferBar: null,
      statusOverlay: null,
      controlsSpacer: null,
      clickToPlay: null,
      controlsOverlay: null,
      fullscreenButton: null,
      layoutControls: null,
      isShowingPictureInPictureMessage: false,
      l10n: this.l10n,

      textTracksCount: 0,
      videoEvents: [
        "play",
        "pause",
        "ended",
        "volumechange",
        "loadeddata",
        "loadstart",
        "timeupdate",
        "progress",
        "playing",
        "waiting",
        "canplay",
        "canplaythrough",
        "seeking",
        "seeked",
        "emptied",
        "loadedmetadata",
        "error",
        "suspend",
        "stalled",
        "mozvideoonlyseekbegin",
        "mozvideoonlyseekcompleted",
        "durationchange",
      ],

      firstFrameShown: false,
      timeUpdateCount: 0,
      maxCurrentTimeSeen: 0,
      isPausedByDragging: false,
      _isAudioOnly: false,

      get isAudioTag() {
        return this.video.localName == "audio";
      },

      get isAudioOnly() {
        return this._isAudioOnly;
      },
      set isAudioOnly(val) {
        this._isAudioOnly = val;
        this.setFullscreenButtonState();
        this.updatePictureInPictureToggleDisplay();

        if (!this.isTopLevelSyntheticDocument) {
          return;
        }
        if (this._isAudioOnly) {
          this.video.style.height = this.controlBarMinHeight + "px";
          this.video.style.width = "66%";
        } else {
          this.video.style.removeProperty("height");
          this.video.style.removeProperty("width");
        }
      },

      suppressError: false,

      setupStatusFader(immediate) {
        // Since the play button will be showing, we don't want to
        // show the throbber behind it. The throbber here will
        // only show if needed after the play button has been pressed.
        if (!this.clickToPlay.hidden) {
          this.startFadeOut(this.statusOverlay, true);
          return;
        }

        var show = false;
        if (
          this.video.seeking ||
          (this.video.error && !this.suppressError) ||
          this.video.networkState == this.video.NETWORK_NO_SOURCE ||
          (this.video.networkState == this.video.NETWORK_LOADING &&
            (this.video.paused || this.video.ended
              ? this.video.readyState < this.video.HAVE_CURRENT_DATA
              : this.video.readyState < this.video.HAVE_FUTURE_DATA)) ||
          (this.timeUpdateCount <= 1 &&
            !this.video.ended &&
            this.video.readyState < this.video.HAVE_FUTURE_DATA &&
            this.video.networkState == this.video.NETWORK_LOADING)
        ) {
          show = true;
        }

        // Explicitly hide the status fader if this
        // is audio only until bug 619421 is fixed.
        if (this.isAudioOnly) {
          show = false;
        }

        if (this._showThrobberTimer) {
          show = true;
        }

        this.log(
          "Status overlay: seeking=" +
            this.video.seeking +
            " error=" +
            this.video.error +
            " readyState=" +
            this.video.readyState +
            " paused=" +
            this.video.paused +
            " ended=" +
            this.video.ended +
            " networkState=" +
            this.video.networkState +
            " timeUpdateCount=" +
            this.timeUpdateCount +
            " _showThrobberTimer=" +
            this._showThrobberTimer +
            " --> " +
            (show ? "SHOW" : "HIDE")
        );
        this.startFade(this.statusOverlay, show, immediate);
      },

      /*
       * Set the initial state of the controls. The UA widget is normally created along
       * with video element, but could be attached at any point (eg, if the video is
       * removed from the document and then reinserted). Thus, some one-time events may
       * have already fired, and so we'll need to explicitly check the initial state.
       */

      setupInitialState() {
        this.setPlayButtonState(this.video.paused);

        this.setFullscreenButtonState();

        var duration = Math.round(this.video.duration * 1000); // in ms
        var currentTime = Math.round(this.video.currentTime * 1000); // in ms
        this.log(
          "Initial playback position is at " + currentTime + " of " + duration
        );
        // It would be nice to retain maxCurrentTimeSeen, but it would be difficult
        // to determine if the media source changed while we were detached.
        this.maxCurrentTimeSeen = currentTime;
        this.showPosition(currentTime, duration);

        // If we have metadata, check if this is a <video> without
        // video data, or a video with no audio track.
        if (this.video.readyState >= this.video.HAVE_METADATA) {
          if (
            this.video.localName == "video" &&
            (this.video.videoWidth == 0 || this.video.videoHeight == 0)
          ) {
            this.isAudioOnly = true;
          }

          // We have to check again if the media has audio here.
          let noAudio = !this.isAudioOnly && !this.video.mozHasAudio;
          this.muteButton.toggleAttribute("noAudio", noAudio);
          this.muteButton.disabled = noAudio;
        }

        // The video itself might not be fullscreen, but part of the
        // document might be, in which case we set this attribute to
        // apply any styles for the DOM fullscreen case.
        if (this.document.fullscreenElement) {
          this.videocontrols.setAttribute("inDOMFullscreen"true);
        }

        if (this.isAudioOnly) {
          this.startFadeOut(this.clickToPlay, true);
        }

        // If the first frame hasn't loaded, kick off a throbber fade-in.
        if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) {
          this.firstFrameShown = true;
        }

        // We can't determine the exact buffering status, but do know if it's
        // fully loaded. (If it's still loading, it will fire a progress event
        // and we'll figure out the exact state then.)
        this.bufferBar.max = 100;
        if (this.video.readyState >= this.video.HAVE_METADATA) {
          this.showBuffered();
        } else {
          this.bufferBar.value = 0;
        }

        // Set the current status icon.
        if (this.hasError()) {
          this.startFadeOut(this.clickToPlay, true);
          this.statusIcon.setAttribute("type""error");
          this.updateErrorText();
          this.setupStatusFader(true);
        }

        this.updatePictureInPictureMessage();

        if (this.video.readyState >= this.video.HAVE_METADATA) {
          // According to the spec[1], at the HAVE_METADATA (or later) state, we know
          // the video duration and dimensions, which means we can calculate whether or
          // not to show the Picture-in-Picture toggle now.
          //
          // [1]: https://www.w3.org/TR/html50/embedded-content-0.html#dom-media-have_metadata
          this.updatePictureInPictureToggleDisplay();
        }

        let adjustableControls = [
          ...this.prioritizedControls,
          this.controlBar,
          this.clickToPlay,
        ];

        for (let control of adjustableControls) {
          if (!control) {
            break;
          }

          this.defineControlProperties(control);
        }
        this.adjustControlSize();

        // Can only update the volume controls once we've computed
        // _volumeControlWidth, since the volume slider implementation
        // depends on it.
        this.updateVolumeControls();
      },

      defineControlProperties(control) {
        let throwOnGet = {
          get() {
            throw new Error("Please don't trigger reflow. See bug 1493525.");
          },
        };
        Object.defineProperties(control, {
          // We should directly access CSSOM to get pre-defined style instead of
          // retrieving computed dimensions from layout.
          minWidth: {
            get: () => {
              let controlId = control.id;
              let propertyName = `--${controlId}-width`;
              if (control.modifier) {
                propertyName += "-" + control.modifier;
              }
              let preDefinedSize =
                this.controlBarComputedStyles.getPropertyValue(propertyName);

              // The stylesheet from <link> might not be loaded if the
              // element was inserted into a hidden iframe.
              // We can safely return 0 here for now, given that the controls
              // will be resized again, by the resizevideocontrols event,
              // from nsVideoFrame, when the element is visible.
              if (!preDefinedSize) {
                return 0;
              }

              return parseInt(preDefinedSize, 10);
            },
          },
          offsetLeft: throwOnGet,
          offsetTop: throwOnGet,
          offsetWidth: throwOnGet,
          offsetHeight: throwOnGet,
          offsetParent: throwOnGet,
          clientLeft: throwOnGet,
          clientTop: throwOnGet,
          clientWidth: throwOnGet,
          clientHeight: throwOnGet,
          getClientRects: throwOnGet,
          getBoundingClientRect: throwOnGet,
          isAdjustableControl: {
            value: true,
          },
          modifier: {
            value: "",
            writable: true,
          },
          isWanted: {
            value: true,
            writable: true,
          },
          hidden: {
            set: v => {
              control._isHiddenExplicitly = v;
              control._updateHiddenAttribute();
            },
            get: () => {
              return (
                control.hasAttribute("hidden") ||
                control.classList.contains("fadeout")
              );
            },
          },
          hiddenByAdjustment: {
            set: v => {
              control._isHiddenByAdjustment = v;
              control._updateHiddenAttribute();
            },
            get: () => control._isHiddenByAdjustment,
          },
          _isHiddenByAdjustment: {
            value: false,
            writable: true,
          },
          _isHiddenExplicitly: {
            value: false,
            writable: true,
          },
          _updateHiddenAttribute: {
            value: () => {
              control.toggleAttribute(
                "hidden",
                control._isHiddenExplicitly || control._isHiddenByAdjustment
              );
            },
          },
        });
      },

      updatePictureInPictureToggleDisplay() {
        if (this.isAudioOnly) {
          this.pictureInPictureToggle.hidden = true;
          return;
        }

        // We only want to show the toggle when the closed captions menu
        // is closed, in order to avoid visual overlap.
        if (
          this.pipToggleEnabled &&
          !this.isShowingPictureInPictureMessage &&
          this.textTrackListContainer.hidden &&
          VideoControlsWidget.shouldShowPictureInPictureToggle(
            this.prefs,
            this.video,
            this.reflowedDimensions
          )
        ) {
          this.pictureInPictureToggle.hidden = false;
          VideoControlsWidget.setupToggle(
            this.prefs,
            this.pictureInPictureToggle,
            this.reflowedDimensions
          );
        } else {
          this.pictureInPictureToggle.hidden = true;
        }
      },

      setupNewLoadState() {
        // For videos with |autoplay| set, we'll leave the controls initially hidden,
        // so that they don't get in the way of the playing video. Otherwise we'll
        // go ahead and reveal the controls now, so they're an obvious user cue.
        var shouldShow =
          !this.dynamicControls || (this.video.paused && !this.video.autoplay);
        // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107.
        let shouldClickToPlayShow =
          shouldShow &&
          !this.isAudioOnly &&
          this.video.currentTime == 0 &&
          !this.hasError() &&
          !this.isShowingPictureInPictureMessage;
        this.startFade(this.clickToPlay, shouldClickToPlayShow, true);
        this.startFade(this.controlBar, shouldShow, true);
      },

      get dynamicControls() {
        // Don't fade controls for <audio> elements.
        var enabled = !this.isAudioOnly;

        // Allow tests to explicitly suppress the fading of controls.
        if (this.video.hasAttribute("mozNoDynamicControls")) {
          enabled = false;
        }

        // If the video hits an error, suppress controls if it
        // hasn't managed to do anything else yet.
        if (!this.firstFrameShown && this.hasError()) {
          enabled = false;
        }

        return enabled;
      },

      updateVolume() {
        const volume = this.volumeControl.value;
        this.setVolume(volume / 100);
      },

      updateVolumeControls() {
        var volume = this.video.muted ? 0 : this.video.volume;
        var volumePercentage = Math.round(volume * 100);
        this.updateMuteButtonState();
        this.volumeControl.value = volumePercentage;
      },

      /*
       * We suspend a video element's video decoder if the video
       * element is invisible. However, resuming the video decoder
       * takes time and we show the throbber UI if it takes more than
       * 250 ms.
       *
       * When an already-suspended video element becomes visible, we
       * resume its video decoder immediately and queue a video-only seek
       * task to seek the resumed video decoder to the current position;
       * meanwhile, we also file a "mozvideoonlyseekbegin" event which
       * we used to start the timer here.
       *
       * Once the queued seek operation is done, we dispatch a
       * "canplay" event which indicates that the resuming operation
       * is completed.
       */

      SHOW_THROBBER_TIMEOUT_MS: 250,
      _showThrobberTimer: null,
      _delayShowThrobberWhileResumingVideoDecoder() {
        this._showThrobberTimer = this.window.setTimeout(() => {
          this.statusIcon.setAttribute("type""throbber");
          // Show the throbber immediately since we have waited for SHOW_THROBBER_TIMEOUT_MS.
          // We don't want to wait for another animation delay(750ms) and the
          // animation duration(300ms).
          this.setupStatusFader(true);
        }, this.SHOW_THROBBER_TIMEOUT_MS);
      },
      _cancelShowThrobberWhileResumingVideoDecoder() {
        if (this._showThrobberTimer) {
          this.window.clearTimeout(this._showThrobberTimer);
          this._showThrobberTimer = null;
        }
      },

      handleEvent(aEvent) {
        if (!aEvent.isTrusted) {
          this.log("Drop untrusted event ----> " + aEvent.type);
          return;
        }

        this.log("Got event ----> " + aEvent.type);

        if (this.videoEvents.includes(aEvent.type)) {
          this.handleVideoEvent(aEvent);
        } else {
          this.handleControlEvent(aEvent);
        }
      },

      handleVideoEvent(aEvent) {
        switch (aEvent.type) {
          case "play":
            this.setPlayButtonState(false);
            this.setupStatusFader();
            if (
              !this._triggeredByControls &&
              this.dynamicControls &&
              this.isTouchControls
            ) {
              this.startFadeOut(this.controlBar);
            }
            if (!this._triggeredByControls) {
              this.startFadeOut(this.clickToPlay, true);
            }
            this._triggeredByControls = false;
            break;
          case "pause":
            // Little white lie: if we've internally paused the video
            // while dragging the scrubber, don't change the button state.
            if (!this.scrubber.isDragging) {
              this.setPlayButtonState(true);
            }
            this.setupStatusFader();
            break;
          case "ended":
            this.setPlayButtonState(true);
            // We throttle timechange events, so the thumb might not be
            // exactly at the end when the video finishes.
            this.showPosition(
              Math.round(this.video.currentTime * 1000),
              Math.round(this.video.duration * 1000)
            );
            this.startFadeIn(this.controlBar);
            this.setupStatusFader();
            break;
          case "volumechange":
            this.updateVolumeControls();
            // Show the controls to highlight the changing volume,
            // but only if the click-to-play overlay has already
            // been hidden (we don't hide controls when the overlay is visible).
            if (this.clickToPlay.hidden && !this.isAudioOnly) {
              this.startFadeIn(this.controlBar);
              this.window.clearTimeout(this._hideControlsTimeout);
              this._hideControlsTimeout = this.window.setTimeout(
                () => this._hideControlsFn(),
                this.HIDE_CONTROLS_TIMEOUT_MS
              );
            }
            break;
          case "loadedmetadata": {
            // If a <video> doesn't have any video data, treat it as <audio>
            // and show the controls (they won't fade back out)
            if (
              this.video.localName == "video" &&
              (this.video.videoWidth == 0 || this.video.videoHeight == 0)
            ) {
              this.isAudioOnly = true;
              this.startFadeOut(this.clickToPlay, true);
              this.startFadeIn(this.controlBar);
              this.setFullscreenButtonState();
            }
            this.showPosition(
              Math.round(this.video.currentTime * 1000),
              Math.round(this.video.duration * 1000)
            );
            let noAudio = !this.isAudioOnly && !this.video.mozHasAudio;
            this.muteButton.toggleAttribute("noAudio", noAudio);
            this.muteButton.disabled = noAudio;
            this.adjustControlSize();
            this.updatePictureInPictureToggleDisplay();
            break;
          }
          case "durationchange":
            this.updatePictureInPictureToggleDisplay();
            break;
          case "loadeddata":
            this.firstFrameShown = true;
            this.setupStatusFader();
            break;
          case "loadstart":
            this.maxCurrentTimeSeen = 0;
            this.controlsSpacer.removeAttribute("aria-label");
            this.statusOverlay.removeAttribute("status");
            this.statusIcon.setAttribute("type""throbber");
            this.isAudioOnly = this.isAudioTag;
            this.setPlayButtonState(true);
            this.setupNewLoadState();
            this.setupStatusFader();
            break;
          case "progress":
            this.statusIcon.removeAttribute("stalled");
            this.showBuffered();
            this.setupStatusFader();
            break;
          case "stalled":
            this.statusIcon.setAttribute("stalled""true");
            this.statusIcon.setAttribute("type""throbber");
            this.setupStatusFader();
            break;
          case "suspend":
            this.setupStatusFader();
            break;
          case "timeupdate":
            var currentTime = Math.round(this.video.currentTime * 1000); // in ms
            var duration = Math.round(this.video.duration * 1000); // in ms

            // If playing/seeking after the video ended, we won't get a "play"
            // event, so update the button state here.
            if (!this.video.paused) {
              this.setPlayButtonState(false);
            }

            this.timeUpdateCount++;
            // Whether we show the statusOverlay sometimes depends
            // on whether we've seen more than one timeupdate
            // event (if we haven't, there hasn't been any
            // "playback activity" and we may wish to show the
            // statusOverlay while we wait for HAVE_ENOUGH_DATA).
            // If we've seen more than 2 timeupdate events,
            // the count is no longer relevant to setupStatusFader.
            if (this.timeUpdateCount <= 2) {
              this.setupStatusFader();
            }

            // If the user is dragging the scrubber ignore the delayed seek
            // responses (don't yank the thumb away from the user)
            if (this.scrubber.isDragging) {
              return;
            }
            this.showPosition(currentTime, duration);
            this.showBuffered();
            break;
          case "emptied":
            this.bufferBar.value = 0;
            this.showPosition(0, 0);
            break;
          case "seeking":
            this.showBuffered();
            this.statusIcon.setAttribute("type""throbber");
            this.setupStatusFader();
            break;
          case "waiting":
            this.statusIcon.setAttribute("type""throbber");
            this.setupStatusFader();
            break;
          case "seeked":
          case "playing":
          case "canplay":
          case "canplaythrough":
            this.setupStatusFader();
            break;
          case "error":
            // We'll show the error status icon when we receive an error event
            // under either of the following conditions:
            // 1. The video has its error attribute set; this means we're loading
            //    from our src attribute, and the load failed, or we we're loading
            //    from source children and the decode or playback failed after we
            //    determined our selected resource was playable.
            // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're
            //    loading from child source elements, but we were unable to select
            //    any of the child elements for playback during resource selection.
            if (this.hasError()) {
              this.suppressError = false;
              this.startFadeOut(this.clickToPlay, true);
              this.statusIcon.setAttribute("type""error");
              this.updateErrorText();
              this.setupStatusFader(true);
              // If video hasn't shown anything yet, disable the controls.
              if (!this.firstFrameShown && !this.isAudioOnly) {
                this.startFadeOut(this.controlBar);
              }
              this.controlsSpacer.removeAttribute("hideCursor");
            }
            break;
          case "mozvideoonlyseekbegin":
            this._delayShowThrobberWhileResumingVideoDecoder();
            break;
          case "mozvideoonlyseekcompleted":
            this._cancelShowThrobberWhileResumingVideoDecoder();
            this.setupStatusFader();
            break;
          default:
            this.log("!!! media event " + aEvent.type + " not handled!");
        }
      },

      handleControlEvent(aEvent) {
        switch (aEvent.type) {
          case "click":
            switch (aEvent.currentTarget) {
              case this.muteButton:
                this.toggleMute();
                break;
              case this.castingButton:
                this.toggleCasting();
                break;
              case this.closedCaptionButton:
                this.toggleClosedCaption();
                break;
              case this.fullscreenButton:
                this.toggleFullscreen();
                break;
              case this.playButton:
              case this.clickToPlay:
              case this.controlsSpacer:
                this.clickToPlayClickHandler(aEvent);
                break;
              case this.textTrackList:
                const index = +aEvent.originalTarget.getAttribute("index");
                this.changeTextTrack(index);
                this.closedCaptionButton.focus();
                break;
              case this.videocontrols:
                // Prevent any click event within media controls from dispatching through to video.
                aEvent.stopPropagation();
                break;
            }
            break;
          case "dblclick":
            this.toggleFullscreen();
            break;
          case "resizevideocontrols":
            // Since this event come from the layout, this is the only place
            // we are sure of that probing into layout won't trigger or force
            // reflow.
            // FIXME(emilio): We should rewrite this to just use
            // ResizeObserver, probably.
            this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = true;
            this.updateReflowedDimensions();
            this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = false;

            let scrubberWasHidden = this.scrubberStack.hidden;
            this.adjustControlSize();
            if (scrubberWasHidden && !this.scrubberStack.hidden) {
              // requestAnimationFrame + setTimeout of 0ms is a best effort way to avoid
              // triggering reflows, but cannot fully guarantee a reflow will not happen.
              this.window.requestAnimationFrame(() =>
                this.window.setTimeout(() => {
                  this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = true;
                  this.updateReflowedDimensions();
                  this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = false;
                }, 0)
              );
            }
            this.updatePictureInPictureToggleDisplay();
            break;
          case "fullscreenchange":
            this.onFullscreenChange();
            break;
          case "keypress":
            this.keyHandler(aEvent);
            break;
          case "dragstart":
            aEvent.preventDefault(); // prevent dragging of controls image (bug 517114)
            break;
          case "input":
            switch (aEvent.currentTarget) {
              case this.scrubber:
                this.onScrubberInput(aEvent);
                break;
              case this.volumeControl:
                this.updateVolume();
                break;
            }
            break;
          case "change":
            switch (aEvent.currentTarget) {
              case this.scrubber:
                this.onScrubberChange(aEvent);
                break;
              case this.video.textTracks:
                this.setClosedCaptionButtonState();
                break;
            }
            break;
          case "mouseup":
            // add mouseup listener additionally to handle the case that `change` event
            // isn't fired when the input value before/after dragging are the same. (bug 1328061)
            this.onScrubberChange(aEvent);
            break;
          case "addtrack":
            this.onTextTrackAdd(aEvent);
            break;
          case "removetrack":
            this.onTextTrackRemove(aEvent);
            break;
          case "media-videoCasting":
            this.updateCasting(aEvent.detail);
            break;
          case "focusin":
            // Show the controls to highlight the focused control, but only
            // under certain conditions:
            if (
              this.prefs["media.videocontrols.keyboard-tab-to-all-controls"] &&
              // The click-to-play overlay must already be hidden (we don't
              // hide controls when the overlay is visible).
              this.clickToPlay.hidden &&
              // Don't do this if the controls are static.
              this.dynamicControls &&
              // If the mouse is hovering over the control bar, the controls
              // are already showing and they shouldn't hide, so don't mess
              // with them.
              // We use "div:hover" instead of just ":hover" so this works in
              // quirks mode documents. See
              // https://quirks.spec.whatwg.org/#the-active-and-hover-quirk
              !this.controlBar.matches("div:hover")
            ) {
              this.startFadeIn(this.controlBar);
              this.window.clearTimeout(this._hideControlsTimeout);
              this._hideControlsTimeout = this.window.setTimeout(
                () => this._hideControlsFn(),
                this.HIDE_CONTROLS_TIMEOUT_MS
              );
            }
            break;
          case "mousedown":
            // We only listen for mousedown on sliders.
            // If this slider isn't focused already, mousedown will focus it.
            // We don't want that because it will then handle additional keys.
            // For example, we don't want the up/down arrow keys to seek after
            // the scrubber is clicked. To prevent that, we need to redirect
            // focus. However, dragging only works while the slider is focused,
            // so we must redirect focus after mouseup.
            if (
              this.prefs["media.videocontrols.keyboard-tab-to-all-controls"] &&
              !aEvent.currentTarget.matches(":focus")
            ) {
              aEvent.currentTarget.addEventListener(
                "mouseup",
                aEvent => {
                  if (aEvent.currentTarget.matches(":focus")) {
                    // We can't use target.blur() because that will blur the
                    // video element as well.
                    this.video.focus();
                  }
                },
                { once: true }
              );
            }
            break;
          default:
            this.log("!!! control event " + aEvent.type + " not handled!");
        }
      },

      terminate() {
        if (this.videoEvents) {
          for (let event of this.videoEvents) {
            try {
              this.video.removeEventListener(event, this, {
                capture: true,
                mozSystemGroup: true,
              });
            } catch (ex) {}
          }
        }

        try {
          for (let { el, type, capture = false } of this.controlsEvents) {
            el.removeEventListener(type, this, {
              mozSystemGroup: true,
              capture,
            });
          }
        } catch (ex) {}

        this.window.clearTimeout(this._showControlsTimeout);
        this.window.clearTimeout(this._hideControlsTimeout);
        this._cancelShowThrobberWhileResumingVideoDecoder();

        this.log("--- videocontrols terminated ---");
      },

      hasError() {
        // We either have an explicit error, or the resource selection
        // algorithm is running and we've tried to load something and failed.
        // Note: we don't consider the case where we've tried to load but
        // there's no sources to load as an error condition, as sites may
        // do this intentionally to work around requires-user-interaction to
        // play restrictions, and we don't want to display a debug message
        // if that's the case.
        return (
          this.video.error != null ||
          (this.video.networkState == this.video.NETWORK_NO_SOURCE &&
            this.hasSources())
        );
      },

      updatePictureInPictureMessage() {
        let showMessage =
          !this.hasError() &&
          VideoControlsWidget.isPictureInPictureVideo(this.video);
        this.pictureInPictureOverlay.hidden = !showMessage;
        this.isShowingPictureInPictureMessage = showMessage;
      },

      hasSources() {
        if (
          this.video.hasAttribute("src") &&
          this.video.getAttribute("src") !== ""
        ) {
          return true;
        }
        for (
          var child = this.video.firstChild;
          child !== null;
          child = child.nextElementSibling
        ) {
          if (child instanceof this.window.HTMLSourceElement) {
            return true;
          }
        }
        return false;
      },

      updateErrorText() {
        let error;
        let v = this.video;
        // It is possible to have both v.networkState == NETWORK_NO_SOURCE
        // as well as v.error being non-null. In this case, we will show
        // the v.error.code instead of the v.networkState error.
        if (v.error) {
          switch (v.error.code) {
            case v.error.MEDIA_ERR_ABORTED:
              error = "errorAborted";
              break;
            case v.error.MEDIA_ERR_NETWORK:
              error = "errorNetwork";
              break;
            case v.error.MEDIA_ERR_DECODE:
              error = "errorDecode";
              break;
            case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
              error =
                v.networkState == v.NETWORK_NO_SOURCE
                  ? "errorNoSource"
                  : "errorSrcNotSupported";
              break;
            default:
              error = "errorGeneric";
              break;
          }
        } else if (v.networkState == v.NETWORK_NO_SOURCE) {
          error = "errorNoSource";
        } else {
          return// No error found.
        }

        let label = this.shadowRoot.getElementById(error);
        this.controlsSpacer.setAttribute("aria-label", label.textContent);
        this.statusOverlay.setAttribute("status", error);
      },

      formatTime(aTime) {
        aTime = Math.round(aTime / 1000);
        let hours = Math.floor(aTime / 3600);
        let mins = Math.floor((aTime % 3600) / 60);
        let secs = Math.floor(aTime % 60);
        let timeString;
        if (secs < 10) {
          secs = "0" + secs;
        }
        if (hours) {
          if (mins < 10) {
            mins = "0" + mins;
          }
          timeString = hours + ":" + mins + ":" + secs;
        } else {
          timeString = mins + ":" + secs;
        }
        return timeString;
      },

      pauseVideoDuringDragging() {
        if (
          !this.video.paused &&
          !this.isPausedByDragging &&
          this.scrubber.isDragging
        ) {
          this.isPausedByDragging = true;
          this.video.pause();
        }
      },

      onScrubberInput() {
        const duration = Math.round(this.video.duration * 1000); // in ms
        let time = this.scrubber.value;

        this.seekToPosition(time);
        this.showPosition(time, duration);
        this.updateScrubberProgress();

        this.scrubber.isDragging = true;
        this.pauseVideoDuringDragging();
      },

      onScrubberChange() {
        this.scrubber.isDragging = false;

        if (this.isPausedByDragging) {
          this.video.play();
          this.isPausedByDragging = false;
        }
      },

      updateScrubberProgress() {
        const positionPercent = (this.scrubber.value / this.scrubber.max) * 100;

        if (!isNaN(positionPercent) && positionPercent != Infinity) {
          this.progressBar.value = positionPercent;
        } else {
          this.progressBar.value = 0;
        }
      },

      seekToPosition(newPosition) {
        newPosition /= 1000; // convert from ms
        this.log("+++ seeking to " + newPosition);
        this.video.currentTime = newPosition;
      },

      setVolume(newVolume) {
        this.log("*** setting volume to " + newVolume);
        this.video.volume = newVolume;
        this.video.muted = false;
      },

      showPosition(currentTimeMs, durationMs) {
        // If the duration is unknown (because the server didn't provide
        // it, or the video is a stream), then we want to fudge the duration
        // by using the maximum playback position that's been seen.
        if (currentTimeMs > this.maxCurrentTimeSeen) {
          this.maxCurrentTimeSeen = currentTimeMs;
        }
        this.log(
          "time update @ " + currentTimeMs + "ms of " + durationMs + "ms"
        );

        let durationIsInfinite = durationMs == Infinity;
        if (isNaN(durationMs) || durationIsInfinite) {
          durationMs = this.maxCurrentTimeSeen;
        }
        this.log("durationMs is " + durationMs + "ms.\n");

        let scrubberProgress = Math.abs(
          currentTimeMs / durationMs - this.scrubber.value / this.scrubber.max
        );
        let devPxProgress =
          scrubberProgress *
          this.reflowedDimensions.scrubberWidth *
          this.window.devicePixelRatio;
        // Hack: if we haven't updated the scrubber width to be non-0, but
        // the scrubber stack is visible, assume there is progress.
        // This should be rectified by the next time we do layout (see handling
        // of resizevideocontrols events in handleEvent).
        if (
          !this.reflowedDimensions.scrubberWidth &&
          !this.scrubberStack.hidden
        ) {
          devPxProgress = 1;
        }
        // Update the scrubber only if it will move by at least 1 pixel
        // Note that this.scrubber.max can be "" if unitialized,
        // and either or both of currentTimeMs or durationMs can be 0, leading
        // to NaN or Infinity values for devPxProgress.
        if (!this.scrubber.max || isNaN(devPxProgress) || devPxProgress > 0.5) {
          this.scrubber.max = durationMs;
          this.scrubber.value = currentTimeMs;
          this.updateScrubberProgress();
        }

        // If the duration is over an hour, thumb should show h:mm:ss instead
        // of mm:ss, which makes it bigger. We set the modifier prop which
        // informs CSS custom properties used elsewhere to determine minimum
        // widths we need to show stuff.
        let modifier = durationMs >= 3600000 ? "long" : "";
        this.positionDurationBox.modifier = this.durationSpan.modifier =
          modifier;

        // Update the text-based labels:
        let position = this.formatTime(currentTimeMs);
        let duration = durationIsInfinite ? "" : this.formatTime(durationMs);
        if (
          this.positionString != position ||
          this.durationString != duration
        ) {
          // Only update the DOM if there is a visible change.
          this._updatePositionLabels(position, duration);
        }
      },

      _updatePositionLabels(position, duration) {
        this.positionString = position;
        this.durationString = duration;

        this.l10n.setAttributes(
          this.positionDurationBox,
          "videocontrols-position-and-duration-labels",
          { position, duration }
        );
        this.l10n.setAttributes(
          this.scrubber,
          "videocontrols-scrubber-position-and-duration",
          { position, duration }
        );
      },

      showBuffered() {
        function bsearch(haystack, needle, cmp) {
          var length = haystack.length;
          var low = 0;
          var high = length;
          while (low < high) {
            var probe = low + ((high - low) >> 1);
            var r = cmp(haystack, probe, needle);
            if (r == 0) {
              return probe;
            } else if (r > 0) {
              low = probe + 1;
            } else {
              high = probe;
            }
          }
          return -1;
        }

        function bufferedCompare(buffered, i, time) {
          if (time > buffered.end(i)) {
            return 1;
          } else if (time >= buffered.start(i)) {
            return 0;
          }
          return -1;
        }

        var duration = Math.round(this.video.duration * 1000);
        if (isNaN(duration) || duration == Infinity) {
          duration = this.maxCurrentTimeSeen;
        }

        // Find the range that the current play position is in and use that
        // range for bufferBar.  At some point we may support multiple ranges
        // displayed in the bar.
        var currentTime = this.video.currentTime;
        var buffered = this.video.buffered;
        var index = bsearch(buffered, currentTime, bufferedCompare);
        var endTime = 0;
        if (index >= 0) {
          endTime = Math.round(buffered.end(index) * 1000);
        }
        if (this.duration == duration && this.buffered == endTime) {
          // Avoid modifying the DOM if there is no update to show.
          return;
        }

        this.bufferBar.max = this.duration = duration;
        this.bufferBar.value = this.buffered = endTime;
        // Progress bars are automatically reported by screen readers even when
        // they aren't focused, which intrudes on the audio being played.
        // Ideally, we'd just change the a11y role of bufferBar, but there's
        // no role which will let us just expose text via an ARIA attribute.
        // Therefore, we hide bufferBar for a11y and expose the info as
        // off-screen text.
        this.bufferA11yVal.textContent =
          (this.bufferBar.position * 100).toFixed() + "%";
      },

      _controlsHiddenByTimeout: false,
      _showControlsTimeout: 0,
      SHOW_CONTROLS_TIMEOUT_MS: 500,
      _showControlsFn() {
        if (this.video.matches("video:hover")) {
          this.startFadeIn(this.controlBar, false);
          this._showControlsTimeout = 0;
          this._controlsHiddenByTimeout = false;
        }
      },

      _hideControlsTimeout: 0,
      _hideControlsFn() {
        if (!this.scrubber.isDragging) {
          this.startFade(this.controlBar, false);
          this._hideControlsTimeout = 0;
          this._controlsHiddenByTimeout = true;
        }
      },
      HIDE_CONTROLS_TIMEOUT_MS: 2000,

      // By "Video" we actually mean the video controls container,
      // because we don't want to consider the padding of <video> added
      // by the web content.
      isMouseOverVideo(event) {
        // XXX: this triggers reflow too, but the layout should only be dirty
        // if the web content touches it while the mouse is moving.
        let el = this.shadowRoot.elementFromPoint(event.clientX, event.clientY);

        // As long as this is not null, the cursor is over something within our
        // Shadow DOM.
        return !!el;
      },

      isMouseOverControlBar(event) {
        // XXX: this triggers reflow too, but the layout should only be dirty
        // if the web content touches it while the mouse is moving.
        let el = this.shadowRoot.elementFromPoint(event.clientX, event.clientY);
        while (el && el !== this.shadowRoot) {
          if (el == this.controlBar) {
            return true;
          }
          el = el.parentNode;
        }
        return false;
      },

      onMouseMove(event) {
        // If the controls are static, don't change anything.
        if (!this.dynamicControls) {
          return;
        }

        this.window.clearTimeout(this._hideControlsTimeout);

        // Suppress fading out the controls until the video has rendered
        // its first frame. But since autoplay videos start off with no
        // controls, let them fade-out so the controls don't get stuck on.
        if (!this.firstFrameShown && !this.video.autoplay) {
          return;
        }

        if (this._controlsHiddenByTimeout) {
          this._showControlsTimeout = this.window.setTimeout(
            () => this._showControlsFn(),
            this.SHOW_CONTROLS_TIMEOUT_MS
          );
        } else {
          this.startFade(this.controlBar, true);
        }

        // Hide the controls if the mouse cursor is left on top of the video
        // but above the control bar and if the click-to-play overlay is hidden.
        if (
          (this._controlsHiddenByTimeout ||
            !this.isMouseOverControlBar(event)) &&
          this.clickToPlay.hidden
        ) {
          this._hideControlsTimeout = this.window.setTimeout(
            () => this._hideControlsFn(),
            this.HIDE_CONTROLS_TIMEOUT_MS
          );
        }
      },

      onMouseInOut(event) {
        // If the controls are static, don't change anything.
        if (!this.dynamicControls) {
          return;
        }

        this.window.clearTimeout(this._hideControlsTimeout);

        let isMouseOverVideo = this.isMouseOverVideo(event);

        // Suppress fading out the controls until the video has rendered
        // its first frame. But since autoplay videos start off with no
        // controls, let them fade-out so the controls don't get stuck on.
        if (
          !this.firstFrameShown &&
          !isMouseOverVideo &&
          !this.video.autoplay
        ) {
          return;
        }

        if (!isMouseOverVideo && !this.isMouseOverControlBar(event)) {
          this.adjustControlSize();

          // Keep the controls visible if the click-to-play is visible.
          if (!this.clickToPlay.hidden) {
            return;
          }

          this.startFadeOut(this.controlBar, false);
          this.hideClosedCaptionMenu();
          this.window.clearTimeout(this._showControlsTimeout);
          this._controlsHiddenByTimeout = false;
        }
      },

      startFadeIn(element, immediate) {
        this.startFade(element, true, immediate);
      },

      startFadeOut(element, immediate) {
        this.startFade(element, false, immediate);
      },

      animationMap: new WeakMap(),

      animationProps: {
        clickToPlay: {
          keyframes: [
            { transform: "scale(3)", opacity: 0 },
            { transform: "scale(1)", opacity: 0.55 },
          ],
          options: {
            easing: "ease",
            duration: 400,
            // The fill mode here and below is a workaround to avoid flicker
            // due to bug 1495350.
            fill: "both",
          },
        },
        controlBar: {
          keyframes: [{ opacity: 0 }, { opacity: 1 }],
          options: {
            easing: "ease",
            duration: 200,
            fill: "both",
          },
        },
        statusOverlay: {
          keyframes: [
            { opacity: 0 },
            { opacity: 0, offset: 0.72 }, // ~750ms into animation
            { opacity: 1 },
          ],
          options: {
            duration: 1050,
            fill: "both",
          },
        },
      },

      startFade(element, fadeIn, immediate = false) {
        let animationProp = this.animationProps[element.id];
        if (!animationProp) {
          throw new Error(
            "Element " +
              element.id +
              " has no transition. Toggle the hidden property directly."
          );
        }

        let animation = this.animationMap.get(element);
        if (!animation) {
          animation = new this.window.Animation(
            new this.window.KeyframeEffect(
              element,
              animationProp.keyframes,
              animationProp.options
            )
          );

          this.animationMap.set(element, animation);
        }

        if (fadeIn) {
          if (element == this.controlBar) {
            this.controlsSpacer.removeAttribute("hideCursor");
            // Ensure the Full Screen button is in the tab order.
            this.fullscreenButton.removeAttribute("tabindex");
          }

          // hidden state should be controlled by adjustControlSize
          if (element.isAdjustableControl && element.hiddenByAdjustment) {
            return;
          }

          // No need to fade in again if the hidden property returns false
          // (not hidden and not fading out.)
          if (!element.hidden) {
            return;
          }

          // Unhide
          element.hidden = false;
        } else {
          if (element == this.controlBar) {
            if (!this.hasError() && this.isVideoInFullScreen) {
              this.controlsSpacer.setAttribute("hideCursor"true);
            }
            if (
              !this.prefs["media.videocontrols.keyboard-tab-to-all-controls"]
            ) {
              // The Full Screen button is currently the only tabbable button
              // when the controls are shown. Remove it from the tab order when
              // visually hidden to prevent visual confusion.
              this.fullscreenButton.setAttribute("tabindex""-1");
            }
          }

          // No need to fade out if the hidden property returns true
          // (hidden or is fading out)
          if (element.hidden) {
            return;
          }
        }

        element.classList.toggle("fadeout", !fadeIn);
        element.classList.toggle("fadein", fadeIn);
        let finishedPromise;
        if (!immediate) {
          // At this point, if there is a pending animation, we just stop it to avoid it happening.
          // If there is a running animation, we reverse it, to have it rewind to the beginning.
          // If there is an idle/finished animation, we schedule a new one that reverses the finished one.
          if (animation.pending) {
            // Animation is running but pending.
            // Just cancel the pending animation to stop its effect.
            animation.cancel();
            finishedPromise = Promise.resolve();
          } else {
            switch (animation.playState) {
              case "idle":
              case "finished":
                // There is no animation currently playing.
                // Schedule a new animation with the desired playback direction.
                animation.playbackRate = fadeIn ? 1 : -1;
                animation.play();
                break;
              case "running":
                // Allow the animation to play from its current position in
                // reverse to finish.
                animation.reverse();
                break;
              case "pause":
                throw new Error("Animation should never reach pause state.");
              default:
                throw new Error(
                  "Unknown Animation playState: " + animation.playState
                );
            }
            finishedPromise = animation.finished;
          }
        } else {
          // immediate
          animation.cancel();
          finishedPromise = Promise.resolve();
        }
        finishedPromise.then(
          animation => {
            if (element == this.controlBar) {
              this.onControlBarAnimationFinished();
            }
            element.classList.remove(fadeIn ? "fadein" : "fadeout");
            if (!fadeIn) {
              element.hidden = true;
            }
            if (animation) {
              // Explicitly clear the animation effect so that filling animations
              // stop overwriting stylesheet styles. Remove when bug 1495350 is
              // fixed and animations are no longer filling animations.
              // This also stops them from accumulating (See bug 1253476).
              animation.cancel();
            }
          },
          () => {
            /* Do nothing on rejection */
          }
        );
      },

      _triggeredByControls: false,

      startPlay() {
        this._triggeredByControls = true;
        this.hideClickToPlay();
        this.video.play();
      },

      togglePause() {
        if (this.video.paused || this.video.ended) {
          this.startPlay();
        } else {
          this.video.pause();
        }

        // We'll handle style changes in the event listener for
        // the "play" and "pause" events, same as if content
        // script was controlling video playback.
      },

      get isVideoWithoutAudioTrack() {
        return (
          this.video.readyState >= this.video.HAVE_METADATA &&
          !this.isAudioOnly &&
          !this.video.mozHasAudio
        );
      },

      toggleMute() {
        if (this.isVideoWithoutAudioTrack) {
          return;
        }
        this.video.muted = !this.isEffectivelyMuted;
        if (this.video.volume === 0) {
          this.video.volume = 0.5;
        }

        // We'll handle style changes in the event listener for
        // the "volumechange" event, same as if content script was
        // controlling volume.
      },

      get isVideoInFullScreen() {
        return this.video.isSameNode(
          this.video.getRootNode().fullscreenElement
        );
      },

      toggleFullscreen() {
        // audio tags cannot toggle fullscreen
        if (!this.isAudioTag) {
          this.isVideoInFullScreen
            ? this.document.exitFullscreen()
            : this.video.requestFullscreen();
        }
      },

      setFullscreenButtonState() {
        if (this.isAudioOnly || !this.document.fullscreenEnabled) {
          this.controlBar.setAttribute("fullscreen-unavailable"true);
          this.adjustControlSize();
          return;
        }
        this.controlBar.removeAttribute("fullscreen-unavailable");
        this.adjustControlSize();

        var id = this.isVideoInFullScreen
          ? "videocontrols-exitfullscreen-button"
          : "videocontrols-enterfullscreen-button";
        this.l10n.setAttributes(this.fullscreenButton, id);

        if (this.isVideoInFullScreen) {
          this.fullscreenButton.setAttribute("fullscreened""true");
        } else {
          this.fullscreenButton.removeAttribute("fullscreened");
        }
      },

      onFullscreenChange() {
        if (this.document.fullscreenElement) {
          this.videocontrols.setAttribute("inDOMFullscreen"true);
        } else {
          this.videocontrols.removeAttribute("inDOMFullscreen");
        }

        if (this.isVideoInFullScreen) {
          this.startFadeOut(this.controlBar, true);
        }

        this.setFullscreenButtonState();
      },

      clickToPlayClickHandler(e) {
        if (e.button != 0) {
          return;
        }
        if (this.hasError() && !this.suppressError) {
          // Errors that can be dismissed should be placed here as we discover them.
          if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED) {
            return;
          }
          this.startFadeOut(this.statusOverlay, true);
          this.suppressError = true;
          return;
        }
        if (e.defaultPrevented) {
          return;
        }
        if (this.playButton.hasAttribute("paused")) {
          this.startPlay();
        } else {
          this.video.pause();
        }
      },
      hideClickToPlay() {
        let videoHeight = this.reflowedDimensions.videoHeight;
        let videoWidth = this.reflowedDimensions.videoWidth;

        // The play button will animate to 3x its size. This
        // shows the animation unless the video is too small
        // to show 2/3 of the animation.
        let animationScale = 2;
        let animationMinSize = this.clickToPlay.minWidth * animationScale;

        let immediate =
          animationMinSize > videoWidth ||
          animationMinSize > videoHeight - this.controlBarMinHeight;
        this.startFadeOut(this.clickToPlay, immediate);
      },

      setPlayButtonState(aPaused) {
        if (aPaused) {
          this.playButton.setAttribute("paused""true");
        } else {
          this.playButton.removeAttribute("paused");
        }

        var id = aPaused
          ? "videocontrols-play-button"
          : "videocontrols-pause-button";
        this.l10n.setAttributes(this.playButton, id);
        this.l10n.setAttributes(this.clickToPlay, id);
      },

      get isEffectivelyMuted() {
        return this.video.muted || !this.video.volume;
      },

      updateMuteButtonState() {
        var muted = this.isEffectivelyMuted;
        this.muteButton.toggleAttribute("muted", muted);

        var id = muted
          ? "videocontrols-unmute-button"
          : "videocontrols-mute-button";
        this.l10n.setAttributes(this.muteButton, id);
      },

      keyboardVolumeDecrease() {
        const oldval = this.video.volume;
        this.video.volume = oldval < 0.1 ? 0 : oldval - 0.1;
        this.video.muted = false;
      },

      keyboardVolumeIncrease() {
        const oldval = this.video.volume;
        this.video.volume = oldval > 0.9 ? 1 : oldval + 0.1;
        this.video.muted = false;
      },

      keyboardSeekBack(tenPercent) {
        const oldval = this.video.currentTime;
        let newval;
        if (tenPercent) {
          newval =
            oldval -
            (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10;
        } else {
          newval = oldval - VideoControlsWidget.SEEK_TIME_SECS;
        }
        this.video.currentTime = Math.max(0, newval);
      },

      keyboardSeekForward(tenPercent) {
        const oldval = this.video.currentTime;
        const maxtime = this.video.duration || this.maxCurrentTimeSeen / 1000;
        let newval;
        if (tenPercent) {
          newval = oldval + maxtime / 10;
        } else {
          newval = oldval + VideoControlsWidget.SEEK_TIME_SECS;
        }
        this.video.currentTime = Math.min(newval, maxtime);
      },

      keyHandler(event) {
        // Ignore keys when content might be providing its own.
        if (!this.video.hasAttribute("controls")) {
          return;
        }

        let keystroke = "";
        if (event.altKey) {
          keystroke += "alt-";
        }
        if (event.shiftKey) {
          keystroke += "shift-";
        }
        if (this.window.navigator.platform.startsWith("Mac")) {
          if (event.metaKey) {
            keystroke += "accel-";
          }
          if (event.ctrlKey) {
            keystroke += "control-";
          }
        } else {
          if (event.metaKey) {
            keystroke += "meta-";
          }
          if (event.ctrlKey) {
            keystroke += "accel-";
          }
        }
        if (event.key == " ") {
          keystroke += "Space";
        } else {
          keystroke += event.key;
        }

        this.log("Got keystroke: " + keystroke);

        // If unmodified cursor keys are pressed when a slider is focused, we
        // should act on that slider. For example, if we're focused on the
        // volume slider, rightArrow should increase the volume, not seek.
        // Normally, we'd just pass the keys through to the slider in this case.
        // However, the native adjustment is too small, so we override it.
        try {
          const target = event.originalTarget;
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=89 H=90 G=89

¤ Dauer der Verarbeitung: 0.36 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.