Facebook Reel: Video Controls

Make Facebook Reel: Video Controls

当前为 2025-10-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Facebook Reel: Video Controls
// @namespace   UserScript
// @match       https://www.facebook.com/*
// @version     0.2.21
// @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 lastXid = "";

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

  };

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

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

  let cid = 0;

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

    if (videoTarget instanceof HTMLVideoElement) {

      if (location.href.indexOf('reel') < 0) return;

      if (lastXid && videoTarget.hasAttribute('controls') && document.querySelector(`[frvc-id="sizing_${lastXid}"]`)){
        return;
      }

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

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

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

      if (cid) clearTimeout(cid);
      cid = setTimeout(() => {

        try {

          document.documentElement.style.removeProperty('--frvc-reel-control-height');
          attributeRemoves(['frvc-debug', 'frvc-holder', 'frvc-css', 'frvc-might-empty']);

          console.log('frvc: checking size');

          setVideoTargetStyles(videoTarget);

          const floatingLayer = [...videoLayerContainer.querySelectorAll('.x10l6tqk.x13vifvy:not(.x1m3v4wt)')].filter(elm => !elm.contains(videoTarget));
          debugInfo.floatingLayer = floatingLayer;

          for (const c of floatingLayer) {
            c.setAttribute("frvc-debug", "clickableHolder-bypass");
          }

          const clickable = videoLayerContainer.querySelectorAll('a[role="link"][href]');
          debugInfo.clickable = clickable;
          const clickableHolder = [...new Set([...clickable].map(e => {
            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;

          // console.log("clickableHolder", clickableHolder);
          const xid = `${Math.floor(Math.random() * 29002921 + 29002921).toString(36)}`;

          const clickableHolderSized = [];
          for (const c of clickableHolder) {
            c.setAttribute("frvc-holder", `m_${xid}`);

            const clickable = c.querySelectorAll('a[role="link"][href]');
            const s = new Set();
            for (let e of clickable) {
              if (!e.parentNode) break;
              do {
                if (e.parentNode === c) break;
                s.add(e.parentNode);
              } while ((e = e.parentNode) instanceof HTMLElement);
            }
            for (const p of s) {
              p.setAttribute("frvc-holder", `s_${xid}`);
            }
            const ret = [[c, c.getBoundingClientRect().height, 0]];
            const elements = c.querySelectorAll(`[frvc-holder="s_${xid}"]`);
            let i = 0;
            for (const element of elements) {
              const p = element.getBoundingClientRect();
              ret.push([element, p.height, ++i]);
            }
            ret.sort((a, b) => b[1] - a[1] || a[2] - b[2]);
            clickableHolderSized.push(ret[0][0]);
          }

          for (const s of floatingLayer) {

            Object.assign(s.style, {
              'pointerEvents': 'none'
            });
            s.setAttribute('frvc-css', "cursor-passthrough");

          }

          for (const s of clickable) {

            Object.assign(s.style, {
              'pointerEvents': 'initial'
            });
          }

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

            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-might-empty', "");
            }
            effctiveHolder.setAttribute("frvc-debug", "sizing");
            lastXid = xid;
            effctiveHolder.setAttribute("frvc-id", `sizing_${lastXid}`);
            resizeObserver.disconnect();
            resizeObserver.observe(effctiveHolder);
          } else {
            console.warn("frvc: sizing element is not found");
          }

        } catch (e) {
          console.error("frvc", e);
        }

      }, 1);

    }

  }, true);

})();