Search code examples
javascriptgoogle-chromegoogle-chrome-extensionhtml5-videovideo.js

Form overlay cannot be filled or submitted on already existing video.js video element


I made a chrome extension that uses a basic form to input specific time stamps onto videos present on web pages. The extension is currently working for both Youtube and Spotify videos as is: I can both enter time stamps (00:00:10) into the form, and submit the form to jump to that time. The problem comes about when trying to use the same extension in tandem with the video.js library.

Although the form will appear over a video.js video as expected, the input is disabled and submitting the form does not render any action.

From what I gather, video.js somehow converts the video in a way that cannot be accessed via the chrome extension. Here is my code for both the content.js and background.js files:

content.js

(function () {
  const OSName = window.navigator.userAgent.includes("Mac")
    ? "Mac/iOS"
    : "Windows/Linux";
  const overlayVisibleBool = { value: false };
  let modalMode;
  let videoEl;

  // let OSName="Mac/iOS";
  let isMoveMutationObserverApplied = false;
  let spotifyOverlayBackground = true;

  const observeUrlChange = () => {
    let oldHref = document.location.href;
    const body = document.querySelector("body");
    const URLObserver = new MutationObserver((mutations) => {
      if (oldHref !== document.location.href) {
        oldHref = document.location.href;
        overlayVisibleBool.value = false;
      }
    });
    URLObserver.observe(body, { childList: true, subtree: true });
  };

  document.addEventListener("DOMContentLoaded", observeUrlChange);

  let fontLinks = {
    DotGothic: `chrome-extension://${chrome.runtime.id}/assets/DotGothic.ttf`,
    Grischel: `chrome-extension://${chrome.runtime.id}/assets/Grischel.ttf`,
    HelNeuMed: `chrome-extension://${chrome.runtime.id}/assets/HelNeuMed.otf`,
    HelNeuMedIt: `chrome-extension://${chrome.runtime.id}/assets/HelNeuMedIt.otf`,
  };

  function addPreloadStyle() {
    addPreloadFont(fontLinks["DotGothic"]);
    addPreloadFont(fontLinks["Grischel"]);
    addPreloadFont(fontLinks["HelNeuMed"]);
    addPreloadFont(fontLinks["HelNeuMedIt"]);

    function addPreloadFont(href) {
      const linkElement = document.createElement("link");
      linkElement.rel = "preload";
      linkElement.href = href;
      linkElement.as = "font";
      linkElement.crossOrigin = "";

      document.head.appendChild(linkElement);
    }
  }

  function addDialogStyles() {
    const style = document.createElement("style");
    style.textContent = `
      @font-face {
        font-family: 'DotGothic';
        src: url(${fontLinks["DotGothic"]})
      }

      @font-face {
        font-family: 'Grischel';
        src: url(${fontLinks["Grischel"]})
      }

      @font-face {
        font-family: 'HelNeuMed';
        src: url(${fontLinks["HelNeuMed"]})
      }

      @font-face {
        font-family: 'HelNeuMedIt';
        src: url(${fontLinks["HelNeuMedIt"]})
      }

      dialog::backdrop {
        position: absolute;
        top: 0px;
        right: 0px;
        bottom: 0px;
        left: 0px;
        background: rgba(0, 0, 0, 0.0);
      }

      dialog:-internal-dialog-in-top-layer {
        max-width: 100%;
        max-height: 100%;
      }

      #timeInput::placeholder {
        color: rgba(222, 220, 220, .25);
      }

      dialog:not([open]) {
        pointer-events: none;
        opacity: 0;
        display: none;
      }    

      #timeInput::selection {
        background: rgba(252, 255, 255, .25);
      }

      #linkButton:hover {
        box-shadow: 0 0 .05vw ${redMeta},
        0 0 .1vw ${redMeta},
        0 0 .2vw ${redMeta},
        0 0 .6vw ${redMeta},
        0 0 .8vw ${redMeta}
      }

      #linkButton:disabled {
        pointer-events: none;
        opacity: .55;
      }

      #timeButton:hover {
        box-shadow: 0 0 .05vw ${redMeta},
        0 0 .1vw ${redMeta},
        0 0 .2vw ${redMeta},
        0 0 .6vw ${redMeta},
        0 0 .8vw ${redMeta}
      }

      #submitButton:hover {
        box-shadow: 0 0 .05vw ${redMeta},
        0 0 .1vw ${redMeta},
        0 0 .2vw ${redMeta},
        0 0 .6vw ${redMeta},
        0 0 .8vw ${redMeta}
      }

      .tippy-content {
        font-size: .5vw;
      }
    `;

    document.head.appendChild(style);
  }

  addPreloadStyle();
  addDialogStyles();

  function toggleOverlay() {
    const videoSizing = window.location.href.includes("watch")
      ? document.getElementById("movie_player")
      : window.location.href.includes("redbar")
      ? // ? videojs('player').el()
        document.getElementById("player_html5_api")
      : document.querySelector("video");
    const popup = document.getElementById("rr_overlay");
    // videoEl = videoSizing;

    if (document.getElementById('player')) {
      // var existingPlayer = videojs('player')
      // var pl = existingPlayer.el();
      videoEl = document.getElementById('player');
      console.log(videoEl.firstChild)
    }

  // Get the parent element of the existing player
    
    // console.log(videoSizing);
    // console.log(window.location.href);

    if (videoSizing && popup) {
      const updateOverlayPosition = () => {
        if (overlayVisibleBool.value) {
          const videoRect = videoSizing.getBoundingClientRect();
          // console.log(videoSizing);
          // console.log(videoRect);
          const popupStyle = popup.style;

          popupStyle.display = "flex";
          popupStyle.left = videoRect.left + "px";
          popupStyle.bottom = videoRect.bottom + "px";
          popupStyle.right = videoRect.right + "px";
          popupStyle.top = videoRect.top + "px";
          popupStyle.width = videoRect.width + "px";
          popupStyle.height = videoRect.height + "px";
          popupStyle.position = "absolute";

          popupStyle.zIndex = 25;

          popupStyle.justifyContent = "center";
          popupStyle.alignItems = "center";
          popupStyle.flexDirection = "column";

          modalMode = document.fullscreenElement ? "showModal" : "show";
          popup[modalMode]();

          popupStyle.opacity = 1;

          const timeInput = document.getElementById("timeInput");
          timeInput && timeInput.focus();
        } else {
          modalMode = "close";
          popup.style.opacity = 0;
          setTimeout(() => {
            popup[modalMode]();
          }, 250);
        }
      };

      updateOverlayPosition();

      if (overlayVisibleBool.value) {
        const resizeObserver = new ResizeObserver(() => {
          const popup = document.getElementById("rr_overlay");
          const videoRect = videoSizing.getBoundingClientRect();
          const popupStyle = popup.style;

          popupStyle.left = videoRect.left + "px";
          popupStyle.bottom = videoRect.bottom + "px";
          popupStyle.right = videoRect.right + "px";
          popupStyle.top = videoRect.top + "px";
          popupStyle.width = videoRect.width + "px";
          popupStyle.height = videoRect.height + "px";

          if (document.fullscreenElement && modalMode === "show" && videoRect) {
            popup.close();
            popup.showModal();
          } else if (
            !document.fullscreenElement &&
            modalMode === "showModal" &&
            videoRect
          ) {
            popup.close();
            popup.show();
          }
        });

        const moveObserver = new MutationObserver(() => {
          const videoRect = videoSizing.getBoundingClientRect();
          const popupStyle = popup.style;

          popupStyle.left = videoRect.left + "px";
          popupStyle.bottom = videoRect.bottom + "px";
          popupStyle.right = videoRect.right + "px";
          popupStyle.top = videoRect.top + "px";
          popupStyle.width = videoRect.width + "px";
          popupStyle.height = videoRect.height + "px";
        });

        if (!isMoveMutationObserverApplied) {
          resizeObserver.observe(videoSizing);
          moveObserver.observe(videoSizing, {
            attributes: true,
            attributeFilter: ["style", "class"],
          });
        }

        isMutationObserverApplied = true;
      }
    }
  }

  const rr_logo = chrome.runtime.getURL("assets/RedbarLogo.svg");
  const link_logo = chrome.runtime.getURL("assets/link_8bit.svg");
  const time_logo = chrome.runtime.getURL("assets/clock_8bit.svg");

  const appendOverlay = () => {
    const overlay = `
    <dialog id="rr_overlay" style="${overlay_style}">
      <div id="rr_container" style="${rr_container}"> 
        <div class="redbar_title" style="${redbar_title}">
          <img src="${rr_logo}" style="${image_style}"/>
          <h1 class="rewind_text" style="${rewind_text}">Rewind®</h1>
        </div>
        <form style="${form_style}" id="jumpForm" method="dialog">
          <input style="${input_style}" autocomplete="off" type="text" id="timeInput" class="timeInput" name="timeInput" placeholder="00:00:00" value="" maxlength="8"/>
          <div class="button_group" style="${buttons_style}">
            <button style="${button_style}" id="submitButton" type="submit" value="jump" name="action"><span>→</span></button>
            <button style="${time_button_style}" id="timeButton" type="submit" value="time" name="time" class="rr_tooltip-trigger"><img src="${time_logo}" style="${time_logo_style}"/></button>
            <button ${
              window.location.href.includes("youtube") ? `` : "disabled"
            } style="${link_button_style}" id="linkButton" type="submit" value="link" name="link" class="rr_tooltip-trigger"><img src="${link_logo}" style="${link_logo_style}"/></button>
          </div>
        </form>
        <small style="${small_style}">© 2023 ALL RIGHTS RESERVED. <span style="font-family: HelNeuMedIt">GIVE IT A DOWNLOAD.</span></small>
      </div>
    </dialog>
  `;
  
    const popupElement = document.createElement("div");
    popupElement.id = "popup_container";
    popupElement.className = "popup_container";
    popupElement.innerHTML = overlay;

    document.body.appendChild(popupElement);
  };

  const appendListeners = () => {
    const jumpForm = document.getElementById("jumpForm");

    jumpForm.addEventListener("submit", function (e) {
      e.preventDefault();
      manageTime(e);
    });

    document
      .getElementById("rr_overlay")
      .addEventListener("click", function (e) {
        if (this === e.target) {
          document.getElementById("timeInput").focus();
          if (
            spotifyOverlayBackground &&
            window.location.href.includes("spotify") &&
            document.fullscreenElement
          ) {
            document.getElementsByClassName(
              "npv-video-overlay"
            )[0].style.opacity = 0;
            document.getElementsByClassName(
              "npv-what-is-playing"
            )[0].style.opacity = 0;
          }
          spotifyOverlayBackground = false;
        }
      });

    document.getElementById("timeButton").addEventListener("click", () => {
      handleClick(handleTimeCopy());
    });

    document.getElementById("linkButton").addEventListener("click", () => {
      handleClick(handleLinkCopy());
    });

    document.getElementById("timeInput").addEventListener("input", (e) => {
      let inputValue = e.target.value;
      inputValue = inputValue.replace(/[^0-9]/g, "");
      inputValue = inputValue.replace(
        /(\d{0,2}):?(\d{0,2})?:?(\d{0,2})?/,
        function (match, p1, p2, p3) {
          return (
            (p1 || "") +
            (p2 ? ":" + (p2.length > 1 ? p2 : p2) : "") +
            (p3 ? ":" + (p3.length > 1 ? p3 : p3) : "")
          );
        }
      );

      e.target.value = inputValue;
    });

    const handleFullscreenChange = () => {
      const popup = document.getElementById("rr_overlay");
      if (
        document.fullscreenElement &&
        overlayVisibleBool.value &&
        modalMode !== "showModal"
      ) {
        // Video is in fullscreen and modal is not in fullscreen mode
        popup.close();
        modalMode = "showModal";
        // console.log("converting to ", modalMode);
        toggleOverlay();
      } else if (
        !document.fullscreenElement &&
        overlayVisibleBool.value &&
        modalMode === "showModal"
      ) {
        // Video is not in fullscreen and modal is in fullscreen mode
        popup.close();
        modalMode = "show";
        // console.log("converting to ", modalMode);
        toggleOverlay();
      }
    };

    document.addEventListener("fullscreenchange", handleFullscreenChange);

    window.addEventListener("beforeunload", function (event) {
      const popup = document.getElementById("rr_overlay");
      popup.style.opacity = 0;
      popup.close();
    });
  };

  const appendTippy = () => {
    const tippyConfig = {
      arrow: false,
      animation: "scale",
      theme: "translucent size",
      inertia: true,
      appendTo: document.getElementById("jumpForm"),
      zIndex: 1000,
    };

    tippy("#timeButton", { ...tippyConfig, content: "Copy Timecode" });
    tippy("#linkButton", { ...tippyConfig, content: "Copy Timestamp Link" });

    const showCopiedTippy = (selector) => {
      tippy(selector, {
        trigger: "click focus",
        content: "Copied!",
        arrow: false,
        animation: "fade",
        theme: "translucent size",
        duration: 200,
        onShow(instance) {
          setTimeout(() => {
            instance.hide();
          }, 1000);
        },
        appendTo: document.getElementById("jumpForm"),
      });
    };

    showCopiedTippy("#timeButton");
    showCopiedTippy("#linkButton");
  };

  appendOverlay();
  appendListeners();
  appendTippy();

  function manageTime(e) {
    const action = e.submitter.value;
    if (action === "jump") {
      const timeInput = document.getElementById("timeInput").value.split(":");
      tl = timeInput.length;

      let hoursInput = 0,
        minutesInput = 0,
        secondsInput = 0;

      if (tl === 1) {
        // Only seconds
        secondsInput = parseInt(timeInput[0], 10) || 0;
      } else if (tl === 2) {
        // Minutes and seconds
        minutesInput = parseInt(timeInput[0], 10) || 0;
        secondsInput = parseInt(timeInput[1], 10) || 0;
      } else if (tl === 3) {
        // Hours, minutes, and seconds
        hoursInput = parseInt(timeInput[0], 10) || 0;
        minutesInput = parseInt(timeInput[1], 10) || 0;
        secondsInput = parseInt(timeInput[2], 10) || 0;
      }

      const totalSeconds = hoursInput * 3600 + minutesInput * 60 + secondsInput;

      // let thevid = document.getElementById('player')
      let thevid = document.getElementsByTagName('video')[0]
      console.log(thevid)
      // console.log(videojs(thevid).el())
      console.log(videojs.getPlayers())
      console.log(videojs.getPlayers(thevid))
      console.log(videojs.players.thevid)

      chrome.runtime?.id &&
        chrome.runtime.sendMessage({
          command: "jumpToTime",
          time: totalSeconds,
          // vid: videoEl
        });

      overlayVisibleBool.value = false;
      toggleOverlay();
    }
  }

  // Key command debug.
  document.addEventListener("keydown", function (event) {
    // console.log(event);
  });

  function handleKeydown(event) {
    const isCtrlKey = OSName === "Mac/iOS" ? event.metaKey : event.ctrlKey;
    const isAltKey = OSName === "Mac/iOS" ? event.ctrlKey : event.altKey;

    if (isCtrlKey && isAltKey) {
      if (
        window.location.href.includes("watch") ||
        window.location.href.includes("open") ||
        window.location.href.includes("redbarradio")
      ) {
        overlayVisibleBool.value = !overlayVisibleBool.value;

        const overlayDiv = document.getElementById("rr_overlay");
        // const player = videojs('player');
        // console.log(videoEl)

        if (!overlayDiv) {
          appendOverlay();
          appendListeners();
          appendTippy();
        }
        if (!spotifyOverlayBackground) {
          spotifyOverlayBackground = true;
        }
        toggleOverlay();
        document.getElementById("timeInput").value = "";
      }
    }
  }

  document.addEventListener("keydown", handleKeydown);

  document.addEventListener(
    "focusin",
    function (event) {
      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
    },
    true
  );
})();

background.js

// console.log("background.js");

function getActiveTabId(callback) {
  chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    if (tabs && tabs.length > 0) {
      const tabId = tabs[0].id;
      callback(tabId);
    }
  });
}

function jumpToTime(time) {
  getActiveTabId((tabId) => {
    chrome.runtime?.id && chrome.scripting.executeScript(
      {
        target: { tabId: tabId },
        func: (time) => {
          // document.addEventListener('DOMContentLoaded', () => {
            const videoElement = document.querySelector("video")
            // videoElement = vid

            console.log(videoElement)

            console.log("JUMPING! ", videoElement, " ", window.location.href.includes("redbar"))

            if (videoElement) {
              videoElement.currentTime = time;
              console.log(videoElement.currentTime);
            }
          // });
        },
        args: [time],
      },
      (result) => handleScriptExecutionResult(result)
    );
  });
}

function handleScriptExecutionResult(result) {
  if (chrome.runtime.lastError) {
    console.error("Background script: Error executing script in content script:", chrome.runtime.lastError);
  }
}

chrome.runtime.onMessage.addListener(async function (message, sender, sendResponse) {
  if (message.command === "jumpToTime") {
    jumpToTime(message.time, message.vid);
  }
  return true;
});

In particular, I'm focusing on these logs in the content.js file.

  // let thevid = document.getElementById('player')
  let thevid = document.getElementsByTagName('video')[0]
  console.log(thevid)
  // console.log(videojs(thevid).el())
  console.log(videojs.getPlayers())
  console.log(videojs.getPlayers(thevid))
  console.log(videojs.players.thevid)

To provide a small summary of how the extension works: a key input opens the form, which automatically accepts number inputs, and the form can be submitted to jump to the time code (converted into seconds) specified.

On sites using the video.js library, both the content.js and background.js files cannot locate the video and the form input cannot be typed into.

Since I couldn't get the video information required to adjust the time stamp, I've tried using every method I could find from the video.js documentation to grab the video information on the page, including .getPlayers() and .players. None have worked so far, using either the content.js (which can access the site) or background.js (which cannot access the site) files.

The only time these methods work is when I uncomment console.log(videojs(thevid).el()), which instantiates the video via the library. That, however, breaks the video as the video already exists on the webpage. Additionally, the documentation frowns on this use of the library.

I've tried working around using the specific library, instead trying to deal directly with the video as is the case with the Youtube and Spotify, but I have not been able to either enter numbers in the input or have the form submit any timestamps including the default 00:00:00 timestamp from an empty form. The video is in essence unmodifiable using the chrome extension as is.

Here is an example video.js site I have been using to test, and here is the submitted Chrome extension in the Web Store that works on both Youtube and Spotify webpages but does not include the additional video.js example site. Additionally, the project is open source so here is the code repository for further testing. Any help is appreciated, including examples of other tools that function with video.js video. To state the obvious, I'm not very familiar with the video.js library. Thank you in advance.


Solution

  • Update:

    Since the player is inside an iframe, you can try something like:

    const videoElement = ('myIframe').contentWindow.document.getElementById('player')
    

    But in your shown page the iframe has no ID by which to target it by, so look for first Div with a class of post-contents. It contains (at [0]) a <p> element which itself holds (at it's own [0] index) the iframe that you want:

    <iframe src="/embed/vod2?id=REDBAR-S21-E23" width="656" is="" height="369" allow="fullscreen" allowfullscreen="" frameborder="0" scrolling="no"></iframe>
    

    Old Answer:

    Your shown example video.js site

    • Is not using VideoJS
    • The media element is not a <video> tag.

    Instead... The <audio> tag looks something like this:

    <audio class="wp-audio-shortcode" id="audio-37196-1" 
    style="width: 100%;" controls="controls" preload="none">
    <source type="audio/mpeg" src="https://..." /> ... </audio>
    

    You should be able to find it with:

    const videoElement = document.querySelector("audio");
    

    Then also this should work (untested assumption):

    if (videoElement) { videoElement.currentTime = time; console.log(videoElement.currentTime); }
    

    PS:
    They seem to be using a player called MediaElement.js
    (scroll down to see their Audio Player demo).

    From checking the sources on Github, it seems to use setCurrentTime for the seek command via:

    setCurrentTime( timeNum_in_Seconds ).

    Hopefully the above gives you some clues towards making your code work as expected.