summaryrefslogtreecommitdiffstats
path: root/toolkit/components/pictureinpicture/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/pictureinpicture/content')
-rw-r--r--toolkit/components/pictureinpicture/content/player.js669
-rw-r--r--toolkit/components/pictureinpicture/content/player.xhtml57
2 files changed, 726 insertions, 0 deletions
diff --git a/toolkit/components/pictureinpicture/content/player.js b/toolkit/components/pictureinpicture/content/player.js
new file mode 100644
index 0000000000..8aeb91bbfe
--- /dev/null
+++ b/toolkit/components/pictureinpicture/content/player.js
@@ -0,0 +1,669 @@
+/* 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.import(
+ "resource://gre/modules/PictureInPicture.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { DeferredTask } = ChromeUtils.import(
+ "resource://gre/modules/DeferredTask.jsm"
+);
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+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";
+
+// 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.jsm. This is the main
+ * entrypoint for initializing the player window.
+ *
+ * @param id (Number)
+ * A unique numeric ID for the window, used for Telemetry Events.
+ * @param wgp (WindowGlobalParent)
+ * The WindowGlobalParent that is hosting the originating video.
+ * @param videoRef {ContentDOMReference}
+ * A reference to the video element that a Picture-in-Picture window
+ * is being created for
+ */
+function setupPlayer(id, wgp, videoRef) {
+ Player.init(id, wgp, videoRef);
+}
+
+/**
+ * Public function to be called from PictureInPicture.jsm. This update the
+ * controls based on whether or not the video is playing.
+ *
+ * @param isPlaying (Boolean)
+ * True if the Picture-in-Picture video is playing.
+ */
+function setIsPlayingState(isPlaying) {
+ Player.isPlaying = isPlaying;
+}
+
+/**
+ * Public function to be called from PictureInPicture.jsm. This update the
+ * controls based on whether or not the video is muted.
+ *
+ * @param isMuted (Boolean)
+ * True if the Picture-in-Picture video is muted.
+ */
+function setIsMutedState(isMuted) {
+ Player.isMuted = isMuted;
+}
+
+/**
+ * The Player object handles initializing the player, holds state, and handles
+ * events for updating state.
+ */
+let Player = {
+ WINDOW_EVENTS: [
+ "click",
+ "contextmenu",
+ "dblclick",
+ "keydown",
+ "mouseup",
+ "MozDOMFullscreen:Entered",
+ "MozDOMFullscreen:Exited",
+ "resize",
+ "unload",
+ ],
+ 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 window movement Telemetry to determine if the player window has
+ * moved since the last time we checked.
+ */
+ lastScreenX: -1,
+ lastScreenY: -1,
+ 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,
+
+ /**
+ * Initializes the player browser, and sets up the initial state.
+ *
+ * @param id (Number)
+ * A unique numeric ID for the window, used for Telemetry Events.
+ * @param wgp (WindowGlobalParent)
+ * The WindowGlobalParent that is hosting the originating video.
+ * @param videoRef {ContentDOMReference}
+ * A reference to the video element that a Picture-in-Picture window
+ * is being created for
+ */
+ init(id, wgp, videoRef) {
+ this.id = id;
+
+ let holder = document.querySelector(".player-holder");
+ let browser = document.getElementById("browser");
+ browser.remove();
+
+ browser.setAttribute("nodefaultsrc", "true");
+
+ // 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);
+ }
+
+ // 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;
+ audioButton.previousElementSibling.hidden = false;
+ }
+
+ Services.telemetry.setEventRecordingEnabled("pictureinpicture", true);
+
+ this.resizeDebouncer = new DeferredTask(() => {
+ this.recordEvent("resize", {
+ width: window.outerWidth.toString(),
+ height: window.outerHeight.toString(),
+ });
+ }, RESIZE_DEBOUNCE_RATE_MS);
+
+ this.lastScreenX = window.screenX;
+ this.lastScreenY = window.screenY;
+
+ this.recordEvent("create", {
+ width: window.outerWidth.toString(),
+ height: window.outerHeight.toString(),
+ screenX: window.screenX.toString(),
+ screenY: window.screenY.toString(),
+ });
+
+ 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.
+ window.requestAnimationFrame(() => {
+ window.focus();
+ });
+ },
+
+ uninit() {
+ this.resizeDebouncer.disarm();
+ PictureInPicture.unload(window, this.actor);
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "click": {
+ 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);
+ } else if (
+ event.keyCode == KeyEvent.DOM_VK_ESCAPE &&
+ this.controls.hasAttribute("keying")
+ ) {
+ this.controls.removeAttribute("keying");
+
+ // We preventDefault to avoid exiting fullscreen if we happen
+ // to be in it.
+ event.preventDefault();
+ } 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;
+ }
+
+ // 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");
+ }
+ });
+ break;
+ }
+
+ case "oop-browser-crashed": {
+ this.closePipWindow({ reason: "browser-crash" });
+ break;
+ }
+
+ case "resize": {
+ this.onResize(event);
+ break;
+ }
+
+ case "unload": {
+ this.uninit();
+ break;
+ }
+ }
+ },
+
+ closePipWindow(closeData) {
+ const { reason } = closeData;
+
+ if (PictureInPicture.isMultiPipEnabled) {
+ PictureInPicture.closeSinglePipWindow({ reason, actorRef: this.actor });
+ } else {
+ PictureInPicture.closeAllPipWindows({ reason });
+ }
+ },
+
+ onDblClick(event) {
+ if (event.target.id == "controls") {
+ if (document.fullscreenElement == document.body) {
+ document.exitFullscreen();
+ } else {
+ document.body.requestFullscreen();
+ }
+ event.preventDefault();
+ }
+ },
+
+ onClick(event) {
+ switch (event.target.id) {
+ case "audio": {
+ if (this.isMuted) {
+ this.actor.sendAsyncMessage("PictureInPicture:Unmute");
+ } else {
+ this.actor.sendAsyncMessage("PictureInPicture:Mute");
+ }
+ break;
+ }
+
+ case "close": {
+ this.actor.sendAsyncMessage("PictureInPicture:Pause", {
+ reason: "pip-closed",
+ });
+ this.closePipWindow({ reason: "close-button" });
+ 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 "unpip": {
+ PictureInPicture.focusTabAndClosePip(window, this.actor);
+ break;
+ }
+ }
+ },
+
+ onKeyDown(event) {
+ this.actor.sendAsyncMessage("PictureInPicture:KeyDown", {
+ altKey: event.altKey,
+ shiftKey: event.shiftKey,
+ metaKey: event.metaKey,
+ ctrlKey: event.ctrlKey,
+ keyCode: event.keyCode,
+ });
+ },
+
+ /**
+ * 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.innerWidth / 2;
+ let windowCenterY = window.screenY + window.innerHeight / 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
+ );
+ },
+
+ 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;
+ },
+
+ onMouseUp(event) {
+ if (
+ window.screenX != this.lastScreenX ||
+ window.screenY != this.lastScreenY
+ ) {
+ this.recordEvent("move", {
+ screenX: window.screenX.toString(),
+ screenY: window.screenY.toString(),
+ });
+ }
+ this.lastScreenX = window.screenX;
+ this.lastScreenY = window.screenY;
+
+ // Corner snapping changes start here.
+ // Check if metakey pressed and macOS
+ let quadrant = this.determineCurrentQuadrant();
+ let dragAction = this.determineDirectionDragged();
+
+ if (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;
+ },
+
+ onResize(event) {
+ this.resizeDebouncer.disarm();
+ this.resizeDebouncer.arm();
+ },
+
+ onCommand(event) {
+ this.closePipWindow({ reason: "player-shortcut" });
+ },
+
+ get controls() {
+ delete this.controls;
+ return (this.controls = document.getElementById("controls"));
+ },
+
+ _isPlaying: false,
+ /**
+ * isPlaying returns true if the video is currently playing.
+ *
+ * @return Boolean
+ */
+ get isPlaying() {
+ return this._isPlaying;
+ },
+
+ /**
+ * Set isPlaying to true if the video is playing, false otherwise. This will
+ * update the internal state and displayed controls.
+ */
+ set isPlaying(isPlaying) {
+ this._isPlaying = isPlaying;
+ this.controls.classList.toggle("playing", isPlaying);
+ const playButton = document.getElementById("playpause");
+ let strId = "pictureinpicture-" + (isPlaying ? "pause" : "play");
+ document.l10n.setAttributes(playButton, strId);
+ },
+
+ _isMuted: false,
+ /**
+ * isMuted returns true if the video is currently muted.
+ *
+ * @return Boolean
+ */
+ get isMuted() {
+ return this._isMuted;
+ },
+
+ /**
+ * Set isMuted to true if the video is muted, false otherwise. This will
+ * update the internal state and displayed controls.
+ */
+ set isMuted(isMuted) {
+ this._isMuted = isMuted;
+ this.controls.classList.toggle("muted", isMuted);
+ const audioButton = document.getElementById("audio");
+ let strId = "pictureinpicture-" + (isMuted ? "unmute" : "mute");
+ document.l10n.setAttributes(audioButton, strId);
+ },
+
+ recordEvent(type, args) {
+ Services.telemetry.recordEvent(
+ "pictureinpicture",
+ type,
+ "player",
+ this.id,
+ args
+ );
+ },
+
+ /**
+ * Makes the player controls visible.
+ *
+ * @param revealIndefinitely (Boolean)
+ * 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 (!revealIndefinitely) {
+ this.showingTimeout = setTimeout(() => {
+ this.controls.removeAttribute("showing");
+ }, 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 width (Number)
+ * The width of the video being played.
+ * @param height (Number)
+ * 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";
+ },
+};
diff --git a/toolkit/components/pictureinpicture/content/player.xhtml b/toolkit/components/pictureinpicture/content/player.xhtml
new file mode 100644
index 0000000000..c0b13a414e
--- /dev/null
+++ b/toolkit/components/pictureinpicture/content/player.xhtml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html>
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="Toolkit:PictureInPicture"
+ chromemargin="0,0,0,0">
+ <head>
+ <meta charset="utf-8"/>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://global/skin/pictureinpicture/player.css"/>
+ <link rel="localization" href="toolkit/pictureinpicture/pictureinpicture.ftl"/>
+ <link rel="localization" href="browser/browserSets.ftl"/>
+ <script src="chrome://global/content/pictureinpicture/player.js"></script>
+ <title data-l10n-id="pictureinpicture-player-title"></title>
+ </head>
+
+ <body>
+ <xul:commandset>
+ <xul:command id="View:PictureInPicture" oncommand="Player.onCommand(event);"/>
+ </xul:commandset>
+
+ <xul:keyset>
+#ifndef XP_MACOSX
+ <xul:key data-l10n-id="picture-in-picture-toggle-shortcut" command="View:PictureInPicture" modifiers="accel,shift"/>
+ <xul:key data-l10n-id="picture-in-picture-toggle-shortcut-alt" command="View:PictureInPicture" modifiers="accel,shift"/>
+#else
+ <xul:key data-l10n-id="picture-in-picture-toggle-shortcut-mac" command="View:PictureInPicture" modifiers="accel,alt,shift"/>
+ <xul:key data-l10n-id="picture-in-picture-toggle-shortcut-mac-alt" command="View:PictureInPicture" modifiers="accel,alt,shift"/>
+#endif
+ </xul:keyset>
+
+ <div class="player-holder">
+ <xul:browser type="content" primary="true" remote="true" remoteType="web" id="browser" tabindex="-1"></xul:browser>
+ </div>
+ <div id="controls">
+ <button id="close"
+ class="control-item"
+#ifdef XP_MACOSX
+ mac="true"
+#endif
+ data-l10n-id="pictureinpicture-close"
+ tabindex="3"/>
+ <div id="controls-bottom">
+ <button id="unpip" class="control-item" data-l10n-id="pictureinpicture-unpip" tabindex="2"></button>
+ <div class="gap"></div>
+ <button id="playpause" class="control-item" tabindex="1" autofocus="true"
+ data-l10n-id="pictureinpicture-pause"/>
+ <div class="gap" hidden="true"></div>
+ <button id="audio" class="control-item" hidden="true" tabindex="1"
+ data-l10n-id="pictureinpicture-mute"/>
+ </div>
+ </div>
+ </body>
+</html>