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

Quelle  player.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/. */


const { PictureInPicture } = ChromeUtils.importESModule(
  "resource://gre/modules/PictureInPicture.sys.mjs"
);
const { ShortcutUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/ShortcutUtils.sys.mjs"
);
const { DeferredTask } = ChromeUtils.importESModule(
  "resource://gre/modules/DeferredTask.sys.mjs"
);
const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

const AUDIO_TOGGLE_ENABLED_PREF =
  "media.videocontrols.picture-in-picture.audio-toggle.enabled";
const KEYBOARD_CONTROLS_ENABLED_PREF =
  "media.videocontrols.picture-in-picture.keyboard-controls.enabled";
const CAPTIONS_ENABLED_PREF =
  "media.videocontrols.picture-in-picture.display-text-tracks.enabled";
const CAPTIONS_TOGGLE_ENABLED_PREF =
  "media.videocontrols.picture-in-picture.display-text-tracks.toggle.enabled";
const TEXT_TRACK_FONT_SIZE_PREF =
  "media.videocontrols.picture-in-picture.display-text-tracks.size";
const IMPROVED_CONTROLS_ENABLED_PREF =
  "media.videocontrols.picture-in-picture.improved-video-controls.enabled";
const SEETHROUGH_MODE_ENABLED_PREF =
  "media.videocontrols.picture-in-picture.seethrough-mode.enabled";

// Time to fade the Picture-in-Picture video controls after first opening.
const CONTROLS_FADE_TIMEOUT_MS = 3000;
const RESIZE_DEBOUNCE_RATE_MS = 500;

/**
Quadrants!
* 2 | 1
* 3 | 4
*/

const TOP_RIGHT_QUADRANT = 1;
const TOP_LEFT_QUADRANT = 2;
const BOTTOM_LEFT_QUADRANT = 3;
const BOTTOM_RIGHT_QUADRANT = 4;

/**
 * Public function to be called from PictureInPicture.sys.mjs. This is the main
 * entrypoint for initializing the player window.
 *
 * @param {Number} id
 *   A unique numeric ID for the window, used for Telemetry Events.
 * @param {WindowGlobalParent} wgp
 *   The WindowGlobalParent that is hosting the originating video.
 * @param {ContentDOMReference} videoRef
 *    A reference to the video element that a Picture-in-Picture window
 *    is being created for
 */

function setupPlayer(id, wgp, videoRef, autoFocus) {
  Player.init(id, wgp, videoRef, autoFocus);
}

/**
 * Public function to be called from PictureInPicture.sys.mjs. This update the
 * controls based on whether or not the video is playing.
 *
 * @param {Boolean} isPlaying
 *   True if the Picture-in-Picture video is playing.
 */

function setIsPlayingState(isPlaying) {
  Player.isPlaying = isPlaying;
}

/**
 * Public function to be called from PictureInPicture.sys.mjs. This update the
 * controls based on whether or not the video is muted.
 *
 * @param {Boolean} isMuted
 *   True if the Picture-in-Picture video is muted.
 */

function setIsMutedState(isMuted) {
  Player.isMuted = isMuted;
}

/**
 * Function to resize and reposition the PiP window
 * @param {Object} rect
 *   An object containing `left`, `top`, `width`, and `height` for the PiP
 *   window
 */

function resizeToVideo(rect) {
  Player.resizeToVideo(rect);
}

/**
 * Returns an object containing `left`, `top`, `width`, and `height` of the
 * PiP window before entering fullscreen. Will be null if the PiP window is
 * not in fullscreen.
 */

function getDeferredResize() {
  return Player.deferredResize;
}

function enableSubtitlesButton() {
  Player.enableSubtitlesButton();
}

function disableSubtitlesButton() {
  Player.disableSubtitlesButton();
}

function setScrubberPosition(position) {
  Player.setScrubberPosition(position);
}

function setTimestamp(timeString) {
  Player.setTimestamp(timeString);
}

function setVolume(volume) {
  Player.setVolume(volume);
}

function closeFromForeground() {
  Player.closeFromForeground();
}

/**
 * The Player object handles initializing the player, holds state, and handles
 * events for updating state.
 */

let Player = {
  _isInitialized: false,
  WINDOW_EVENTS: [
    "click",
    "contextmenu",
    "dblclick",
    "keydown",
    "mouseup",
    "mousemove",
    "MozDOMFullscreen:Entered",
    "MozDOMFullscreen:Exited",
    "resize",
    "unload",
    "draggableregionleftmousedown",
  ],
  actor: null,
  /**
   * Used for resizing Telemetry to avoid recording an event for every resize
   * event. Instead, we wait until RESIZE_DEBOUNCE_RATE_MS has passed since the
   * last resize event before recording.
   */

  resizeDebouncer: null,
  /**
   * Used for Telemetry to identify the window.
   */

  id: -1,

  /**
   * When set to a non-null value, a timer is scheduled to hide the controls
   * after CONTROLS_FADE_TIMEOUT_MS milliseconds.
   */

  showingTimeout: null,

  /**
   * Used to determine old window location when mouseup-ed for corner
   * snapping drag vector calculation
   */

  oldMouseUpWindowX: window.screenX,
  oldMouseUpWindowY: window.screenY,

  /**
   * Used to determine if hovering the mouse cursor over the pip window or not.
   * Gets updated whenever a new hover state is detected.
   */

  isCurrentHover: false,

  /**
   * Store the size and position of the window before entering fullscreen and
   * use this to correctly position the window when exiting fullscreen
   */

  deferredResize: null,

  /**
   * Initializes the player browser, and sets up the initial state.
   *
   * @param {Number} id
   *   A unique numeric ID for the window, used for Telemetry Events.
   * @param {WindowGlobalParent} wgp
   *   The WindowGlobalParent that is hosting the originating video.
   * @param {ContentDOMReference} videoRef
   *   A reference to the video element that a Picture-in-Picture window
   *   is being created for
   * @param {boolean} autoFocus
   *   Autofocus the PiP window
   */

  init(id, wgp, videoRef, autoFocus) {
    this.id = id;

    // State for whether or not we are adjusting the time via the scrubber
    this.scrubbing = false;

    let holder = document.querySelector(".player-holder");
    let browser = document.getElementById("browser");
    browser.remove();

    browser.setAttribute("nodefaultsrc""true");

    this.setupTooltip("close""pictureinpicture-close-btn""closeShortcut");
    let strId = this.isFullscreen
      ? `pictureinpicture-exit-fullscreen-btn2`
      : `pictureinpicture-fullscreen-btn2`;
    this.setupTooltip("fullscreen", strId, "fullscreenToggleShortcut");

    // Set the specific remoteType and browsingContextGroupID to use for the
    // initial about:blank load. The combination of these two properties will
    // ensure that the browser loads in the same process as our originating
    // browser.
    browser.setAttribute("remoteType", wgp.domProcess.remoteType);
    browser.setAttribute(
      "initialBrowsingContextGroupId",
      wgp.browsingContext.group.id
    );
    holder.appendChild(browser);

    this.actor =
      browser.browsingContext.currentWindowGlobal.getActor("PictureInPicture");
    this.actor.sendAsyncMessage("PictureInPicture:SetupPlayer", {
      videoRef,
    });

    PictureInPicture.weakPipToWin.set(this.actor, window);

    for (let eventType of this.WINDOW_EVENTS) {
      addEventListener(eventType, this);
    }

    this.controls.addEventListener("mouseleave", () => {
      this.onMouseLeave();
    });
    this.controls.addEventListener("mouseenter", () => {
      this.onMouseEnter();
    });

    this.scrubber.addEventListener("input", event => {
      this.handleScrubbing(event);
    });
    this.scrubber.addEventListener("change", event => {
      this.handleScrubbingDone(event);
    });

    this.audioScrubber.addEventListener("input", event => {
      this.audioScrubbing = true;
      this.handleAudioScrubbing(event.target.value);
    });
    this.audioScrubber.addEventListener("change", () => {
      this.audioScrubbing = false;
    });
    this.audioScrubber.addEventListener("pointerdown", () => {
      if (this.isMuted) {
        this.audioScrubber.max = 1;
      }
    });

    for (let radio of document.querySelectorAll(
      'input[type=radio][name="cc-size"]'
    )) {
      radio.addEventListener("change", event => {
        this.onSubtitleChange(event.target.id);
      });
    }

    document
      .querySelector("#subtitles-toggle")
      .addEventListener("change", () => {
        this.onToggleChange();
      });

    // If the content process hosting the video crashes, let's
    // just close the window for now.
    browser.addEventListener("oop-browser-crashed"this);

    this.revealControls(false);

    if (Services.prefs.getBoolPref(AUDIO_TOGGLE_ENABLED_PREF, false)) {
      const audioButton = document.getElementById("audio");
      audioButton.hidden = false;

      const audioScrubber = document.getElementById("audio-scrubber");
      audioScrubber.hidden = false;
    }

    if (Services.prefs.getBoolPref(CAPTIONS_ENABLED_PREF, false)) {
      this.closedCaptionButton.hidden = false;
    }

    if (Services.prefs.getBoolPref(IMPROVED_CONTROLS_ENABLED_PREF, false)) {
      const fullscreenButton = document.getElementById("fullscreen");
      fullscreenButton.hidden = false;

      const seekBackwardButton = document.getElementById("seekBackward");
      seekBackwardButton.hidden = false;

      const seekForwardButton = document.getElementById("seekForward");
      seekForwardButton.hidden = false;

      this.scrubber.hidden = false;
      this.timestamp.hidden = false;

      const controlsBottomGradient = document.getElementById(
        "controls-bottom-gradient"
      );
      controlsBottomGradient.hidden = false;
    }

    this.alignEndControlsButtonTooltips();

    this.resizeDebouncer = new DeferredTask(() => {
      this.alignEndControlsButtonTooltips();
      this.recordEvent("resize", {
        width: window.outerWidth,
        height: window.outerHeight,
      });
    }, RESIZE_DEBOUNCE_RATE_MS);

    this.computeAndSetMinimumSize(window.outerWidth, window.outerHeight);

    // alwaysontop windows are not focused by default, so we have to do it
    // ourselves. We use requestAnimationFrame since we have to wait until the
    // window is visible before it can focus.
    if (autoFocus) {
      window.requestAnimationFrame(() => {
        window.focus();
      });
    }

    let fontSize = Services.prefs.getCharPref(
      TEXT_TRACK_FONT_SIZE_PREF,
      "medium"
    );

    // fallback to medium if the pref value is not a valid option
    if (fontSize === "small" || fontSize === "large") {
      document.querySelector(`#${fontSize}`).checked = "true";
    } else {
      document.querySelector("#medium").checked = "true";
    }

    // In see-through mode the PiP window is made semi-transparent on hover.
    if (Services.prefs.getBoolPref(SEETHROUGH_MODE_ENABLED_PREF, false)) {
      document.documentElement.classList.add("seethrough-mode");
    }

    this._isInitialized = true;
  },

  uninit() {
    this.resizeDebouncer.disarm();
    PictureInPicture.unload(window, this.actor);
  },

  setupTooltip(elId, l10nId, shortcutId) {
    const el = document.getElementById(elId);
    const shortcut = document.getElementById(shortcutId);
    let l10nObj = shortcut
      ? { shortcut: ShortcutUtils.prettifyShortcut(shortcut) }
      : {};
    document.l10n.setAttributes(el, l10nId, l10nObj);
  },

  handleEvent(event) {
    switch (event.type) {
      case "click": {
        // Don't run onClick if middle or right click is pressed respectively
        if (event.button !== 1 && event.button !== 2) {
          this.onClick(event);
          this.controls.removeAttribute("keying");
        }
        break;
      }

      case "contextmenu": {
        event.preventDefault();
        break;
      }

      case "dblclick": {
        this.onDblClick(event);
        break;
      }

      case "keydown": {
        if (event.keyCode == KeyEvent.DOM_VK_TAB) {
          this.controls.setAttribute("keying"true);
          this.showVideoControls();
        } else if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
          let isSettingsPanelInFocus = this.settingsPanel.contains(
            document.activeElement
          );

          event.preventDefault();

          if (!this.settingsPanel.classList.contains("hide")) {
            // If the subtitles settings panel is open, let the ESC key close it
            this.toggleSubtitlesSettingsPanel({ forceHide: true });
            if (isSettingsPanelInFocus) {
              document.getElementById("closed-caption").focus();
            }
          } else if (this.isFullscreen) {
            // We handle the ESC key, in fullscreen modus as intent to leave only the fullscreen mode
            document.exitFullscreen();
          } else {
            // We handle the ESC key, as an intent to leave the picture-in-picture modus
            this.onClose();
          }
        } else if (
          Services.prefs.getBoolPref(KEYBOARD_CONTROLS_ENABLED_PREF, false) &&
          (event.keyCode != KeyEvent.DOM_VK_SPACE || !event.target.id)
        ) {
          // Pressing "space" fires a "keydown" event which can also trigger a control
          // button's "click" event. Handle the "keydown" event only when the event did
          // not originate from a control button and it is not a "space" keypress.
          this.onKeyDown(event);
        }

        break;
      }

      case "mouseup": {
        this.onMouseUp(event);
        break;
      }

      case "mousemove": {
        this.onMouseMove();
        break;
      }

      // Normally, the DOMFullscreenParent / DOMFullscreenChild actors
      // would take care of firing the `fullscreen-painted` notification,
      // however, those actors are only ever instantiated when a <browser>
      // is fullscreened, and not a <body> element in a parent-process
      // chrome privileged DOM window.
      //
      // Rather than trying to re-engineer JSWindowActors to be re-usable for
      // this edge-case, we do the work of firing fullscreen-painted when
      // transitioning in and out of fullscreen ourselves here.
      case "MozDOMFullscreen:Entered":
      // Intentional fall-through
      case "MozDOMFullscreen:Exited": {
        let { lastTransactionId } = window.windowUtils;
        window.addEventListener("MozAfterPaint"function onPainted(event) {
          if (event.transactionId > lastTransactionId) {
            window.removeEventListener("MozAfterPaint", onPainted);
            Services.obs.notifyObservers(window, "fullscreen-painted");
          }
        });

        // If we are exiting fullscreen we want to resize the window to the
        // stored size and position
        if (this.deferredResize && event.type === "MozDOMFullscreen:Exited") {
          this.resizeToVideo(this.deferredResize);
          this.deferredResize = null;
        }

        // Sets the title for fullscreen button when PIP is in Enter Fullscreen mode and Exit Fullscreen mode
        let strId = this.isFullscreen
          ? `pictureinpicture-exit-fullscreen-btn2`
          : `pictureinpicture-fullscreen-btn2`;
        this.setupTooltip("fullscreen", strId, "fullscreenToggleShortcut");

        window.focus();

        if (this.isFullscreen) {
          this.actor.sendAsyncMessage("PictureInPicture:EnterFullscreen", {
            isFullscreen: true,
            isVideoControlsShowing: null,
            playerBottomControlsDOMRect: null,
          });
        } else {
          this.actor.sendAsyncMessage("PictureInPicture:ExitFullscreen", {
            isFullscreen: this.isFullscreen,
            isVideoControlsShowing:
              !!this.controls.getAttribute("showing") ||
              !!this.controls.getAttribute("keying"),
            playerBottomControlsDOMRect:
              this.controlsBottom.getBoundingClientRect(),
          });
        }
        // The subtitles settings panel gets selected when entering/exiting fullscreen even though
        // user-select is set to none. I don't know why this happens or how to prevent so we just
        // remove the selection when fullscreen is entered/exited.
        let selection = window.getSelection();
        selection.removeAllRanges();
        break;
      }

      case "oop-browser-crashed": {
        this.closePipWindow({ reason: "BrowserCrash" });
        break;
      }

      case "resize": {
        this.onResize(event);
        break;
      }

      case "unload": {
        this.uninit();
        break;
      }

      case "draggableregionleftmousedown": {
        this.toggleSubtitlesSettingsPanel({ forceHide: true });
        break;
      }
    }
  },

  /**
   * This function handles when the scrubber is being scrubbed by the mouse
   * because if we get an input event from the keyboard, onKeyDown will set
   * this.preventNextInputEvent to true.
   * This function is called by input events on the scrubber
   * @param {Event} event The input event
   */

  handleScrubbing(event) {
    // When using the keyboard to scrub, we get both a keydown and an input
    // event. The input event is fired after the keydown and we have already
    // handled the keydown event in onKeyDown so we set preventNextInputEvent
    // to true in onKeyDown as to not set the current time twice.
    if (this.preventNextInputEvent) {
      this.preventNextInputEvent = false;
      return;
    }
    if (!this.scrubbing) {
      this.wasPlaying = this.isPlaying;
      if (this.isPlaying) {
        this.actor.sendAsyncMessage("PictureInPicture:Pause");
      }
      this.scrubbing = true;
    }
    let scrubberPosition = this.getScrubberPositionFromEvent(event);
    this.setVideoTime(scrubberPosition);
  },

  /**
   * This function handles setting the scrubbing state to false and playing
   * the video if we paused it before scrubbing.
   * @param {Event} event The change event
   */

  handleScrubbingDone(event) {
    if (!this.scrubbing) {
      return;
    }
    let scrubberPosition = this.getScrubberPositionFromEvent(event);
    this.setVideoTime(scrubberPosition);
    if (this.wasPlaying) {
      this.actor.sendAsyncMessage("PictureInPicture:Play");
    }
    this.scrubbing = false;
  },

  /**
   * Set the volume on the video and unmute if the video was muted.
   * If the volume is changed via the keyboard, onKeyDown will set
   * this.preventNextInputEvent to true.
   * @param {Number} volume A number between 0 and 1 that represents the volume
   */

  handleAudioScrubbing(volume) {
    // When using the keyboard to adjust the volume, we get both a keydown and
    // an input event. The input event is fired after the keydown event and we
    // have already handled the keydown event in onKeyDown so we set
    // preventNextInputEvent to true in onKeyDown as to not set the volume twice.
    if (this.preventNextInputEvent) {
      this.preventNextInputEvent = false;
      return;
    }

    if (this.isMuted) {
      this.isMuted = false;
      this.actor.sendAsyncMessage("PictureInPicture:Unmute");
    }

    if (volume == 0) {
      this.actor.sendAsyncMessage("PictureInPicture:Mute");
    }

    this.actor.sendAsyncMessage("PictureInPicture:SetVolume", {
      volume,
    });
  },

  getScrubberPositionFromEvent(event) {
    return event.target.value;
  },

  setVideoTime(scrubberPosition) {
    let wasPlaying = this.scrubbing ? this.wasPlaying : this.isPlaying;
    this.setScrubberPosition(scrubberPosition);
    this.actor.sendAsyncMessage("PictureInPicture:SetVideoTime", {
      scrubberPosition,
      wasPlaying,
    });
  },

  setScrubberPosition(value) {
    this.scrubber.value = value;
    this.scrubber.hidden = value === undefined;

    // Also hide the seek buttons when we hide the scrubber
    this.seekBackward.hidden = value === undefined;
    this.seekForward.hidden = value === undefined;
  },

  setTimestamp(timestamp) {
    this.timestamp.textContent = timestamp;
    this.timestamp.hidden = timestamp === undefined;
  },

  setVolume(volume) {
    if (volume < Number.EPSILON) {
      this.actor.sendAsyncMessage("PictureInPicture:Mute");
    }

    this.audioScrubber.value = volume;
  },

  closePipWindow(closeData) {
    // Set the subtitles font size prefs
    Services.prefs.setBoolPref(
      CAPTIONS_TOGGLE_ENABLED_PREF,
      document.querySelector("#subtitles-toggle").checked
    );
    for (let radio of document.querySelectorAll(
      'input[type=radio][name="cc-size"]'
    )) {
      if (radio.checked) {
        Services.prefs.setCharPref(TEXT_TRACK_FONT_SIZE_PREF, radio.id);
        break;
      }
    }
    const { reason } = closeData;
    PictureInPicture.closeSinglePipWindow({ reason, actorRef: this.actor });
  },

  onDblClick(event) {
    if (event.target.id == "controls") {
      this.fullscreenModeToggle();
      event.preventDefault();
    }
  },

  onClick(event) {
    switch (event.target.id) {
      case "audio": {
        this.toggleMute();
        break;
      }

      case "close": {
        this.onClose();
        break;
      }

      case "playpause": {
        if (!this.isPlaying) {
          this.actor.sendAsyncMessage("PictureInPicture:Play");
          this.revealControls(false);
        } else {
          this.actor.sendAsyncMessage("PictureInPicture:Pause");
          this.revealControls(true);
        }

        break;
      }

      case "seekBackward": {
        this.actor.sendAsyncMessage("PictureInPicture:SeekBackward");
        break;
      }

      case "seekForward": {
        this.actor.sendAsyncMessage("PictureInPicture:SeekForward");
        break;
      }

      case "unpip": {
        PictureInPicture.focusTabAndClosePip(window, this.actor);
        break;
      }

      case "closed-caption": {
        let options = {};
        if (event.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
          options.isKeyboard = true;
        }
        this.toggleSubtitlesSettingsPanel(options);
        // Early return to prevent hiding the panel below
        return;
      }

      case "fullscreen": {
        this.fullscreenModeToggle();
        this.recordEvent("fullscreen", {
          enter: !this.isFullscreen,
        });
        break;
      }

      case "font-size-selection-radio-small": {
        document.getElementById("small").click();
        break;
      }

      case "font-size-selection-radio-medium": {
        document.getElementById("medium").click();
        break;
      }

      case "font-size-selection-radio-large": {
        document.getElementById("large").click();
        break;
      }
    }
    // If the click came from a element that is not inside the subtitles settings panel
    // then we want to hide the panel
    if (!this.settingsPanel.contains(event.target)) {
      this.toggleSubtitlesSettingsPanel({ forceHide: true });
    }
  },

  /**
   * Function to toggle the visibility of the subtitles settings panel
   * @param {Object} options [optional] Object containing options for the function
   *   - forceHide: true to force hide the subtitles settings panel
   *   - isKeyboard: true if the subtitles button was activated using the keyboard
   *     to show or hide the subtitles settings panel
   */

  toggleSubtitlesSettingsPanel(options) {
    let settingsPanelVisible = !this.settingsPanel.classList.contains("hide");
    if (options?.forceHide || settingsPanelVisible) {
      this.settingsPanel.classList.add("hide");
      this.closedCaptionButton.setAttribute("aria-expanded"false);
      this.controls.removeAttribute("donthide");

      if (
        this.controls.getAttribute("keying") ||
        this.isCurrentHover ||
        this.controls.getAttribute("showing")
      ) {
        return;
      }

      this.hideVideoControls();
    } else {
      this.settingsPanel.classList.remove("hide");
      this.closedCaptionButton.setAttribute("aria-expanded"true);
      this.controls.setAttribute("donthide"true);
      this.showVideoControls();

      if (options?.isKeyboard) {
        document.querySelector("#subtitles-toggle").focus();
      }
    }
  },

  onClose() {
    this.actor.sendAsyncMessage("PictureInPicture:Pause", {
      reason: "pip-closed",
    });
    this.closePipWindow({ reason: "CloseButton" });
  },

  closeFromForeground() {
    PictureInPicture.closeSinglePipWindow({
      reason: "Foregrounded",
      actorRef: this.actor,
    });
  },

  fullscreenModeToggle() {
    if (this.isFullscreen) {
      document.exitFullscreen();
    } else {
      this.deferredResize = {
        left: window.screenX,
        top: window.screenY,
        width: window.outerWidth,
        height: window.outerHeight,
      };
      document.body.requestFullscreen();
    }
  },

  /**
   * Toggle the mute state of the video
   */

  toggleMute() {
    if (this.isMuted) {
      // We unmute in handleAudioScrubbing so no need to also do it here
      this.audioScrubber.max = 1;
      this.handleAudioScrubbing(this.lastVolume ?? 1);
    } else {
      this.lastVolume = this.audioScrubber.value;
      this.actor.sendAsyncMessage("PictureInPicture:Mute");
    }
  },

  resizeToVideo(rect) {
    if (this.isFullscreen) {
      // We store the size and position because resizing the PiP window
      // while fullscreened will cause issues
      this.deferredResize = rect;
    } else {
      let { left, top, width, height } = rect;
      window.resizeTo(width, height);
      window.moveTo(left, top);
    }
  },

  onKeyDown(event) {
    // We don't want to send a keydown event if the event target was one of the
    // font sizes in the settings panel
    if (
      event.target.parentElement?.parentElement?.classList?.contains(
        "font-size-selection"
      )
    ) {
      return;
    }

    let eventKeys = {
      altKey: event.altKey,
      shiftKey: event.shiftKey,
      metaKey: event.metaKey,
      ctrlKey: event.ctrlKey,
      keyCode: event.keyCode,
    };

    // If the up or down arrow is pressed while the scrubber is focused then we
    // want to hijack these keydown events to act as left or right arrows
    // respectively to correctly seek the video.
    if (
      event.target.id === "scrubber" &&
      event.keyCode === window.KeyEvent.DOM_VK_UP
    ) {
      eventKeys.keyCode = window.KeyEvent.DOM_VK_RIGHT;
    } else if (
      event.target.id === "scrubber" &&
      event.keyCode === window.KeyEvent.DOM_VK_DOWN
    ) {
      eventKeys.keyCode = window.KeyEvent.DOM_VK_LEFT;
    }

    // If the left or right arrow is pressed while the audio scrubber is focused
    // then we want to hijack these keydown events to act as up or down arrows
    // respectively to correctly change the volume.
    if (
      event.target.id === "audio-scrubber" &&
      event.keyCode === window.KeyEvent.DOM_VK_RIGHT
    ) {
      eventKeys.keyCode = window.KeyEvent.DOM_VK_UP;
    } else if (
      event.target.id === "audio-scrubber" &&
      event.keyCode === window.KeyEvent.DOM_VK_LEFT
    ) {
      eventKeys.keyCode = window.KeyEvent.DOM_VK_DOWN;
    }

    // If the keydown event was one of the arrow keys and the scrubber or the
    // audio scrubber was focused then we want to prevent the subsequent input
    // event from overwriting the keydown event.
    if (
      event.target.id === "audio-scrubber" ||
      (event.target.id === "scrubber" &&
        [
          window.KeyEvent.DOM_VK_LEFT,
          window.KeyEvent.DOM_VK_RIGHT,
          window.KeyEvent.DOM_VK_UP,
          window.KeyEvent.DOM_VK_DOWN,
        ].includes(event.keyCode))
    ) {
      this.preventNextInputEvent = true;
    }

    this.actor.sendAsyncMessage("PictureInPicture:KeyDown", eventKeys);
  },

  onSubtitleChange(size) {
    Services.prefs.setCharPref(TEXT_TRACK_FONT_SIZE_PREF, size);

    this.actor.sendAsyncMessage("PictureInPicture:ChangeFontSizeTextTracks");
  },

  onToggleChange() {
    // The subtitles toggle has been click in the settings panel so we toggle
    // the overlay above the font sizes and send a message to toggle the
    // visibility of the subtitles and set the toggle pref
    document
      .querySelector(".font-size-selection")
      .classList.toggle("font-size-overlay");
    this.actor.sendAsyncMessage("PictureInPicture:ToggleTextTracks");

    this.captionsToggleEnabled = !this.captionsToggleEnabled;
    Services.prefs.setBoolPref(
      CAPTIONS_TOGGLE_ENABLED_PREF,
      this.captionsToggleEnabled
    );
  },

  /**
   * PiP Corner Snapping Helper Function
   * Determines the quadrant the PiP window is currently in.
   */

  determineCurrentQuadrant() {
    // Determine center coordinates of window.
    let windowCenterX = window.screenX + window.outerWidth / 2;
    let windowCenterY = window.screenY + window.outerHeight / 2;
    let quadrant = null;
    let halfWidth = window.screen.availLeft + window.screen.availWidth / 2;
    let halfHeight = window.screen.availTop + window.screen.availHeight / 2;

    let leftHalf = windowCenterX < halfWidth;
    let rightHalf = windowCenterX > halfWidth;
    let topHalf = windowCenterY < halfHeight;
    let bottomHalf = windowCenterY > halfHeight;

    if (leftHalf && topHalf) {
      quadrant = TOP_LEFT_QUADRANT;
    } else if (rightHalf && topHalf) {
      quadrant = TOP_RIGHT_QUADRANT;
    } else if (leftHalf && bottomHalf) {
      quadrant = BOTTOM_LEFT_QUADRANT;
    } else if (rightHalf && bottomHalf) {
      quadrant = BOTTOM_RIGHT_QUADRANT;
    }
    return quadrant;
  },

  /**
   * Helper function to actually move/snap the PiP window.
   * Moves the PiP window to the top right.
   */

  moveToTopRight() {
    window.moveTo(
      window.screen.availLeft + window.screen.availWidth - window.innerWidth,
      window.screen.availTop
    );
  },

  /**
   * Moves the PiP window to the top left.
   */

  moveToTopLeft() {
    window.moveTo(window.screen.availLeft, window.screen.availTop);
  },

  /**
   * Moves the PiP window to the bottom right.
   */

  moveToBottomRight() {
    window.moveTo(
      window.screen.availLeft + window.screen.availWidth - window.innerWidth,
      window.screen.availTop + window.screen.availHeight - window.innerHeight
    );
  },

  /**
   * Moves the PiP window to the bottom left.
   */

  moveToBottomLeft() {
    window.moveTo(
      window.screen.availLeft,
      window.screen.availTop + window.screen.availHeight - window.innerHeight
    );
  },

  /**
   * Uses the PiP window's change in position to determine which direction
   * the window has been moved in.
   */

  determineDirectionDragged() {
    // Determine change in window location.
    let deltaX = this.oldMouseUpWindowX - window.screenX;
    let deltaY = this.oldMouseUpWindowY - window.screenY;
    let dragDirection = "";

    if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX < 0) {
      dragDirection = "draggedRight";
    } else if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 0) {
      dragDirection = "draggedLeft";
    } else if (Math.abs(deltaX) < Math.abs(deltaY) && deltaY < 0) {
      dragDirection = "draggedDown";
    } else if (Math.abs(deltaX) < Math.abs(deltaY) && deltaY > 0) {
      dragDirection = "draggedUp";
    }
    return dragDirection;
  },

  /**
   * Event handler for "mouseup" events on the PiP window.
   *
   * @param {Event} event
   *  Event context details
   */

  onMouseUp(event) {
    // Corner snapping changes start here.
    // Check if metakey pressed and macOS
    let quadrant = this.determineCurrentQuadrant();
    let dragAction = this.determineDirectionDragged();

    if (
      ((event.ctrlKey && AppConstants.platform !== "macosx") ||
        (event.metaKey && AppConstants.platform === "macosx")) &&
      dragAction
    ) {
      // Moving logic based on current quadrant and direction of drag.
      switch (quadrant) {
        case TOP_RIGHT_QUADRANT:
          switch (dragAction) {
            case "draggedRight":
              this.moveToTopRight();
              break;
            case "draggedLeft":
              this.moveToTopLeft();
              break;
            case "draggedDown":
              this.moveToBottomRight();
              break;
            case "draggedUp":
              this.moveToTopRight();
              break;
          }
          break;
        case TOP_LEFT_QUADRANT:
          switch (dragAction) {
            case "draggedRight":
              this.moveToTopRight();
              break;
            case "draggedLeft":
              this.moveToTopLeft();
              break;
            case "draggedDown":
              this.moveToBottomLeft();
              break;
            case "draggedUp":
              this.moveToTopLeft();
              break;
          }
          break;
        case BOTTOM_LEFT_QUADRANT:
          switch (dragAction) {
            case "draggedRight":
              this.moveToBottomRight();
              break;
            case "draggedLeft":
              this.moveToBottomLeft();
              break;
            case "draggedDown":
              this.moveToBottomLeft();
              break;
            case "draggedUp":
              this.moveToTopLeft();
              break;
          }
          break;
        case BOTTOM_RIGHT_QUADRANT:
          switch (dragAction) {
            case "draggedRight":
              this.moveToBottomRight();
              break;
            case "draggedLeft":
              this.moveToBottomLeft();
              break;
            case "draggedDown":
              this.moveToBottomRight();
              break;
            case "draggedUp":
              this.moveToTopRight();
              break;
          }
          break;
      } // Switch close.
    } // Metakey close.
    this.oldMouseUpWindowX = window.screenX;
    this.oldMouseUpWindowY = window.screenY;
  },

  /**
   * Event handler for mousemove the PiP Window
   */

  onMouseMove() {
    if (this.isFullscreen) {
      this.revealControls(false);
    }
  },

  onMouseEnter() {
    if (!this.isFullscreen) {
      this.isCurrentHover = true;
      this.showVideoControls();
    }
  },

  onMouseLeave() {
    if (!this.isFullscreen) {
      this.isCurrentHover = false;
      if (
        !this.controls.getAttribute("showing") &&
        !this.controls.getAttribute("keying") &&
        !this.controls.getAttribute("donthide")
      ) {
        this.hideVideoControls();
      }
    }
  },

  enableSubtitlesButton() {
    this.closedCaptionButton.disabled = false;

    this.alignEndControlsButtonTooltips();
    this.captionsToggleEnabled = true;
    // If the CAPTIONS_TOGGLE_ENABLED_PREF pref is false then we will click
    // the UI toggle to change the toggle to unchecked. This will call
    // onToggleChange where this.captionsToggleEnabled will be updated
    if (!Services.prefs.getBoolPref(CAPTIONS_TOGGLE_ENABLED_PREF, true)) {
      document.querySelector("#subtitles-toggle").click();
    }
  },

  disableSubtitlesButton() {
    this.closedCaptionButton.disabled = true;

    this.alignEndControlsButtonTooltips();
  },

  /**
   * Sets focus state inline end tooltip for rightmost playback controls
   */

  alignEndControlsButtonTooltips() {
    let audioBtn = document.getElementById("audio");
    let width = window.outerWidth;

    if (300 < width && width <= 400) {
      audioBtn.classList.replace("center-tooltip""inline-end-tooltip");
    } else {
      audioBtn.classList.replace("inline-end-tooltip""center-tooltip");
    }
  },

  /**
   * Event handler for resizing the PiP Window
   *
   * @param {Event} event
   *  Event context data object
   */

  onResize() {
    this.toggleSubtitlesSettingsPanel({ forceHide: true });
    this.resizeDebouncer.disarm();
    this.resizeDebouncer.arm();
  },

  /**
   * Event handler for user issued commands
   *
   * @param {Event} event
   *  Event context data object
   */

  onCommand() {
    this.closePipWindow({ reason: "Shortcut" });
  },

  get controls() {
    delete this.controls;
    return (this.controls = document.getElementById("controls"));
  },

  get scrubber() {
    delete this.scrubber;
    return (this.scrubber = document.getElementById("scrubber"));
  },

  get audioScrubber() {
    delete this.audioScrubber;
    return (this.audioScrubber = document.getElementById("audio-scrubber"));
  },

  get timestamp() {
    delete this.timestamp;
    return (this.timestamp = document.getElementById("timestamp"));
  },

  get controlsBottom() {
    delete this.controlsBottom;
    return (this.controlsBottom = document.getElementById("controls-bottom"));
  },

  get seekBackward() {
    delete this.seekBackward;
    return (this.seekBackward = document.getElementById("seekBackward"));
  },

  get seekForward() {
    delete this.seekForward;
    return (this.seekForward = document.getElementById("seekForward"));
  },

  get closedCaptionButton() {
    delete this.closedCaptionButton;
    return (this.closedCaptionButton =
      document.getElementById("closed-caption"));
  },

  get settingsPanel() {
    delete this.settingsPanel;
    return (this.settingsPanel = document.getElementById("settings"));
  },

  _isPlaying: false,
  /**
   * GET isPlaying returns true if the video is currently playing.
   *
   * SET isPlaying to true if the video is playing, false otherwise. This will
   * update the internal state and displayed controls.
   *
   * @type {Boolean}
   */

  get isPlaying() {
    return this._isPlaying;
  },

  set isPlaying(isPlaying) {
    this._isPlaying = isPlaying;
    this.controls.classList.toggle("playing", isPlaying);
    let strId = isPlaying
      ? `pictureinpicture-pause-btn`
      : `pictureinpicture-play-btn`;
    this.setupTooltip("playpause", strId);

    if (
      !this._isInitialized ||
      this.isCurrentHover ||
      this.controls.getAttribute("keying")
    ) {
      return;
    }

    if (!isPlaying) {
      this.revealControls(true);
    } else {
      this.revealControls(false);
    }
  },

  _isMuted: false,
  /**
   * GET isMuted returns true if the video is currently muted.
   *
   * SET isMuted to true if the video is muted, false otherwise. This will
   * update the internal state and displayed controls.
   *
   * @type {Boolean}
   */

  get isMuted() {
    return this._isMuted;
  },

  set isMuted(isMuted) {
    this._isMuted = isMuted;
    if (!isMuted) {
      this.audioScrubber.max = 1;
    } else if (!this.audioScrubbing) {
      this.audioScrubber.max = 0;
    }
    this.controls.classList.toggle("muted", isMuted);
    let strId = isMuted
      ? `pictureinpicture-unmute-btn`
      : `pictureinpicture-mute-btn`;
    let shortcutId = isMuted ? "unMuteShortcut" : "muteShortcut";
    this.setupTooltip("audio", strId, shortcutId);
  },

  /**
   * GET isFullscreen returns true if the video is running in fullscreen mode
   *
   * @returns {boolean}
   */

  get isFullscreen() {
    return document.fullscreenElement == document.body;
  },

  /**
   * Used for recording telemetry in Picture-in-Picture.
   *
   * @param {string} type
   *   The type of PiP event being recorded.
   * @param {object} args
   *   The data to pass to telemetry when the event is recorded.
   */

  recordEvent(type, args) {
    args.value = this.id;
    Glean.pictureinpicture[type + "Player"].record(args);
  },

  /**
   * Send a message to PiPChild to adjust the subtitles position
   * so that subtitles are visible when showing video controls.
   */

  showVideoControls() {
    // offsetParent returns null when the element or any ancestor has display: none
    // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
    this.actor.sendAsyncMessage("PictureInPicture:ShowVideoControls", {
      isFullscreen: this.isFullscreen,
      isVideoControlsShowing: true,
      playerBottomControlsDOMRect: this.controlsBottom.getBoundingClientRect(),
      isScrubberShowing: !!this.scrubber.offsetParent,
    });
  },

  /**
   * Send a message to PiPChild to adjust the subtitles position
   * so that subtitles take up remaining space when hiding video controls.
   */

  hideVideoControls() {
    this.actor.sendAsyncMessage("PictureInPicture:HideVideoControls", {
      isFullscreen: this.isFullscreen,
      isVideoControlsShowing: false,
      playerBottomControlsDOMRect: null,
    });
  },

  /**
   * Makes the player controls visible.
   *
   * @param {Boolean} revealIndefinitely
   *   If false, this will hide the controls again after
   *   CONTROLS_FADE_TIMEOUT_MS milliseconds has passed. If true, the controls
   *   will remain visible until revealControls is called again with
   *   revealIndefinitely set to false.
   */

  revealControls(revealIndefinitely) {
    clearTimeout(this.showingTimeout);
    this.showingTimeout = null;

    this.controls.setAttribute("showing"true);

    if (!this.isFullscreen) {
      // revealControls() is called everytime we hover over fullscreen pip window.
      // Only communicate with pipchild when not in fullscreen mode for performance reasons.
      this.showVideoControls();
    }

    if (!revealIndefinitely) {
      this.showingTimeout = setTimeout(() => {
        const isHoverOverControlItem = this.controls.querySelector(
          ".control-item:hover"
        );
        if (this.isFullscreen && isHoverOverControlItem) {
          return;
        }
        this.controls.removeAttribute("showing");

        if (
          !this.isFullscreen &&
          !this.isCurrentHover &&
          !this.controls.getAttribute("keying") &&
          !this.controls.getAttribute("donthide")
        ) {
          this.hideVideoControls();
        }
      }, CONTROLS_FADE_TIMEOUT_MS);
    }
  },

  /**
   * Given a width and height for a video, computes the minimum dimensions for
   * the player window, and then sets them on the root element.
   *
   * This is currently only used on Linux GTK, where the OS doesn't already
   * impose a minimum window size. For other platforms, this function is a
   * no-op.
   *
   * @param {Number} width
   *   The width of the video being played.
   * @param {Number} height
   *   The height of the video being played.
   */

  computeAndSetMinimumSize(width, height) {
    if (!AppConstants.MOZ_WIDGET_GTK) {
      return;
    }

    // Using inspection, these seem to be the right minimums for each dimension
    // so that the controls don't get too crowded.
    const MIN_WIDTH = 120;
    const MIN_HEIGHT = 80;

    let resultWidth = width;
    let resultHeight = height;
    let aspectRatio = width / height;

    // Take the smaller of the two dimensions, and set it to the minimum.
    // Then calculate the other dimension using the aspect ratio to get
    // both minimums.
    if (width < height) {
      resultWidth = MIN_WIDTH;
      resultHeight = Math.round(MIN_WIDTH / aspectRatio);
    } else {
      resultHeight = MIN_HEIGHT;
      resultWidth = Math.round(MIN_HEIGHT * aspectRatio);
    }

    document.documentElement.style.minWidth = resultWidth + "px";
    document.documentElement.style.minHeight = resultHeight + "px";
  },
};

Messung V0.5
C=92 H=85 G=88

¤ Dauer der Verarbeitung: 0.22 Sekunden  (vorverarbeitet)  ¤

*© 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.