Facebook Reel: Video Controls

Make Facebook Reel: Video Controls

// ==UserScript==
// @name        Facebook Reel: Video Controls
// @namespace   UserScript
// @match       https://www.facebook.com/*
// @version     0.2.10
// @license     MIT
// @author      CY Fung
// @description Make Facebook Reel: Video Controls
// @run-at      document-start
// @grant       none
// @unwrap
// ==/UserScript==

(() => {

  const resizeObserver = new ResizeObserver((entries) => {
    for (const entry of entries) {
      if (entry.contentRect.height > 0) {
        document.documentElement.style.setProperty('--frvc-reel-control-height', entry.contentRect.height + 'px');
      }
    }
  });

  let addCSS = () => {
    if (addCSS.done) return;
    addCSS.done = true;
    document.head.appendChild(document.createElement('style')).textContent = `
    [frvc-div-might-empty]:empty {
      display: none;
      
    }
    [frvc-cursor-passthrough] {
      pointer-events: none;
    }
  
    [frvc-cursor-passthrough] [role], [frvc-cursor-passthrough] [tabindex] {
      pointer-events: initial;
    }

    [frvc-mutual-layer]:hover [frvc-video-button-layer]{
      z-index: 9999;
      opacity: initial !important;
      transition-duration: 0 !important;
    }

    [frvc-mutual-layer]:hover [frvc-video-button-layer] .x10l6tqk {
      z-index: 9999;
    }
  
    `;

  };

  const setVideoTargetStyles = (videoTarget) => {
    Object.assign(videoTarget.style, {
      'position': 'relative',
      'zIndex': 999,
      'pointerEvents': 'all',
      'height': 'calc(100% - var(--frvc-reel-control-height))'
    });
  };

  const mutualLayer = (a, b) => {
    let c = a, d = b;
    if (!a || !b) return null;
    while (c || d) {
      if (c && c.contains(b)) {
        return c;
      } else if (d && d.contains(a)) {
        return d;
      }
      if (c) c = c.parentElement;
      if (d) d = d.parentElement;
    }
    return document.body;
  }

  const forceShowButtons = (layer) => {
    const buttons = layer.querySelectorAll('div[aria-label][role="button"][tabindex][class]');
    const floatingLayer = [...new Set([...buttons].map((btn) => btn.closest("div.x1jha2h7")))].filter(e => !!e);
    if (floatingLayer.length === 0) return;
    layer.setAttribute("frvc-mutual-layer", "");
    for (const s of floatingLayer) {
      s.setAttribute("frvc-video-button-layer", "");
    }
  }

  const attributeRemoves = (list) => {
    for (const m of list) {
      for (const s of document.querySelectorAll(`[${m}]`)) {
        s.removeAttribute(m);
      }
    }
  }

  document.addEventListener('play', (evt) => {
    const videoTarget = (evt || 0).target;

    if (videoTarget instanceof HTMLVideoElement) {

      {
        const fid = videoTarget.getAttribute("frvc-video-id");
        if (fid) {
          const layer = document.querySelector(`[frvc-layer-id="${fid}"]`);
          if (!layer) {
            videoTarget.removeAttribute("frvc-video-id");
          } else if (!layer.contains(videoTarget)) {
            videoTarget.removeAttribute("frvc-video-id");
            layer.removeAttribute("frvc-layer-id");
          } else {
            return;
          }
        }
      }

      // if (videoTarget.hasAttribute('controls')) return;
      if (location.href.indexOf('reel') < 0) return;

      const debugInfo = {};
      Promise.resolve(debugInfo).then(console.debug);

      const videoLayerContainer = videoTarget.closest('div[class][role="button"][tabindex], div[role="main"]');
      if (!videoLayerContainer) return;
      debugInfo.videoLayerContainer = videoLayerContainer;

      videoTarget.setAttribute('controls', '');
      addCSS();

      const fid = `f${Math.floor(Math.random() * 1e8 + 1e8).toString(36)}`;

      videoTarget.setAttribute("frvc-video-id", fid);

      setTimeout(() => {

        if (!videoTarget.hasAttribute("controls") || videoTarget.getAttribute("frvc-video-id") !== fid) return;

        document.documentElement.style.removeProperty('--frvc-reel-control-height');
        
        attributeRemoves(["frvc-video-layer-container", "frvc-cursor-passthrough", "frvc-div-might-empty", "frvc-mutual-layer"]);

        // position: absolute; top:0; do not contains videoTarget
        videoLayerContainer.setAttribute("frvc-video-layer-container", "");
        let floatingLayer = [...videoLayerContainer.querySelectorAll('.x10l6tqk.x13vifvy:not(.x1m3v4wt)')].filter(elm => !elm.contains(videoTarget));
        debugInfo.floatingLayer = floatingLayer;


        const clickable = videoLayerContainer.querySelectorAll('a[role="link"][href]');
        debugInfo.clickable = clickable;
        const clickableHolder = [...new Set([...clickable].map(e => {
          // looking for clickableHolder inside floatingLayer
          do {
            if (floatingLayer.includes(e.parentNode)) return e;
          } while ((e = e.parentNode) instanceof HTMLElement);
          return null;
        }))].filter(e => !!e).map(e => {
          const f = (e) => {
            const { firstElementChild, lastElementChild } = e;
            if (firstElementChild === lastElementChild) return f(firstElementChild);
            const validChildren = [...e.children].filter(e => e.firstElementChild !== null);
            if (validChildren.length === 1) return f(validChildren[0]);
            return e;
          }
          return f(e);
        });

        debugInfo.clickableHolder = clickableHolder;

        if (clickableHolder.length === 0) return;

        for (const s of clickableHolder) {
          let e = s;
          while (e && e.nodeType === 1 && !e.hasAttribute("frvc-video-layer-container")) {
            if (floatingLayer.includes(e)) {
              Object.assign(e.style, {
                'pointerEvents': 'none'
              });
              e.setAttribute('frvc-cursor-passthrough', "");
            }
            e = e.parentNode;
          }
          const clickable = s.querySelectorAll('a[role="link"][href]');
          for (const s of clickable) {
            Object.assign(s.style, {
              'pointerEvents': 'initial'
            });
          }
        }


        const videoElmBRect = videoTarget.getBoundingClientRect();
        let effctiveHolder = null;
        for (const s of clickableHolder) {

          if (effctiveHolder === null) {

            const clickableHolderBRect = s.getBoundingClientRect();
            const conditions = {
              bottom: Math.abs(clickableHolderBRect.bottom - videoElmBRect.bottom) < 48,
              top: clickableHolderBRect.top + 1 > videoElmBRect.top,
              left: Math.abs(clickableHolderBRect.left - videoElmBRect.left) < 5,
              right: Math.abs(clickableHolderBRect.right - videoElmBRect.right) < 5
            }
            console.debug(conditions);
            if (conditions.bottom && conditions.top && conditions.left && conditions.right) {
              effctiveHolder = s;
            }
          }

          Object.assign(s.style, {
            'pointerEvents': 'initial',
            'height': 'auto',
            'boxSizing': 'border-box',
            'paddingTop': '16px'
          });
        }

        debugInfo.effctiveHolder = effctiveHolder;

        if (effctiveHolder) {
          addCSS();
          for (const s of effctiveHolder.querySelectorAll('div[class]:empty')) {
            s.setAttribute('frvc-div-might-empty', "");
          }
          resizeObserver.disconnect();
          resizeObserver.observe(effctiveHolder);

          setVideoTargetStyles(videoTarget);

          const layer = mutualLayer(videoTarget, effctiveHolder);
          forceShowButtons(layer);

          for (const s of document.querySelectorAll("[frvc-video-id], [frvc-layer-id]")) {
            s.removeAttribute("frvc-video-id");
            s.removeAttribute("frvc-layer-id");
          }
          videoTarget.setAttribute("frvc-video-id", fid);
          layer.setAttribute("frvc-layer-id", fid);

        }

      }, 1);

    }

  }, true);

})();