Facebook video downloader (2026)

Download any video on Facebook (post/chat/comment)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Facebook video downloader (2026)
// @icon        https://www.facebook.com/favicon.ico
// @namespace   Violentmonkey Scripts
// @match       https://www.facebook.com/*
// @match       https://web.facebook.com/*
// @grant       GM_registerMenuCommand
// @grant       unsafeWindow
// @run-at      document-start
// @version     1.5
// @author      https://github.com/HoangTran0410
// @description Download any video on Facebook (post/chat/comment)
// @license MIT
// ==/UserScript==
 
(() => {
  const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;

  function ensureReactDevToolsHook() {
    if (pageWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__) return;

    let nextRendererId = 0;
    const renderers = new Map();
    const fiberRoots = new Map();

    pageWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
      supportsFiber: true,
      renderers,
      inject(renderer) {
        const rendererId = ++nextRendererId;
        renderers.set(rendererId, renderer);
        fiberRoots.set(rendererId, new Set());
        return rendererId;
      },
      onCommitFiberRoot(rendererId, root) {
        let roots = fiberRoots.get(rendererId);
        if (!roots) {
          roots = new Set();
          fiberRoots.set(rendererId, roots);
        }

        const current = root?.current;
        const isUnmounting =
          current?.memoizedState == null ||
          current?.memoizedState?.element == null;
        if (isUnmounting) {
          roots.delete(root);
          return;
        }

        roots.add(root);
      },
      onCommitFiberUnmount() {},
      getFiberRoots(rendererId) {
        return fiberRoots.get(rendererId) || new Set();
      },
      sub() {
        return function unsubscribe() {};
      },
      checkDCE() {},
    };
  }

  ensureReactDevToolsHook();

  function getOverlapScore(el) {
    var rect = el.getBoundingClientRect();
    return (
      Math.min(
        rect.bottom,
        window.innerHeight || document.documentElement.clientHeight
      ) - Math.max(0, rect.top)
    );
  }
 
  const videoContainerSelector = [
    "[data-video-id]",
    "[data-instancekey]",
    '[data-visualcompletion="ignore"]',
  ].join(",");

  function closestAncestor(element, selector, boundary = null) {
    let el = element;
    while (el && el.nodeType === Node.ELEMENT_NODE) {
      if (el.matches?.(selector)) return el;
      if (boundary && el === boundary) break;
      el = el.parentElement;
    }
    return null;
  }

  function getVideoScope(videoEle) {
    if (!videoEle) return null;
    return (
      closestAncestor(videoEle, videoContainerSelector) ||
      videoEle.parentElement
    );
  }

  const facebookVideoLinkSelector = [
    "a[href*='/reel/']",
    "a[href*='/watch/?v=']",
    "a[href*='/watch?v=']",
    "a[href*='/videos/']",
    "a[href*='video_id=']",
    "a[href*='videoid=']",
  ].join(",");

  function isNumericString(str) {
    return typeof str === "string" && /^[0-9]+$/.test(str);
  }

  function cleanNumericId(value) {
    if (!value) return "";
    const id = String(value).split("?")[0].split("&")[0];
    return isNumericString(id) && id !== "ifu" ? id : "";
  }

  function getVideoIdFromUrl(url) {
    if (!url) return "";

    const href = String(url);
    const parts = href.split("/");
    let idpost = "";

    if (href.includes("/reel/")) {
      idpost = cleanNumericId(parts[4]);
      if (idpost) return idpost;
    }

    if (href.includes("/videos/")) {
      const videoIndex = parts.indexOf("videos");
      idpost = cleanNumericId(parts[videoIndex + 1]);
      if (idpost) return idpost;
    }

    if (href.includes("pagechienca/")) {
      idpost = cleanNumericId(parts[5]);
      if (idpost) return idpost;
    }

    if (href.includes("=")) {
      idpost = cleanNumericId(href.split("=")[1]);
      if (idpost) return idpost;
    }

    idpost = cleanNumericId(parts[5]);
    if (idpost) return idpost;

    idpost = cleanNumericId(parts[6]);
    if (idpost) return idpost;

    return "";
  }

  function getVideoIdFromPageUrl() {
    const reelpost = location.pathname.includes("/reel/");
    const videourl = location.pathname.includes("/videos/");
    const watchurl = location.pathname.startsWith("/watch");

    if (!reelpost && !videourl && !watchurl) return "";

    return getVideoIdFromUrl(location.href);
  }

  function getClosestInstanceKeyElement(videoEle, container) {
    return (
      closestAncestor(videoEle, 'div[data-instancekey^="id-vpuid"]') ||
      closestAncestor(container, 'div[data-instancekey^="id-vpuid"]') ||
      Array.from(
        document.querySelectorAll('div[data-instancekey^="id-vpuid"]')
      ).find((element) => element.contains(videoEle)) ||
      null
    );
  }

  function getVideoIdFromStorySaverDom(videoEle, container) {
    const pageVideoId = getVideoIdFromPageUrl();
    if (pageVideoId) return pageVideoId;

    let max =
      getClosestInstanceKeyElement(videoEle, container) ||
      videoEle?.parentElement ||
      container;
    let maxadd = 0;

    while (max && max !== document.body) {
      const query = max.querySelectorAll?.(facebookVideoLinkSelector);

      if (query?.length) {
        for (let item of query) {
          const idpost = getVideoIdFromUrl(item.href);
          if (idpost) return idpost;
        }
      }

      maxadd += 1;
      if (maxadd > 100) break;
      max = max.parentElement;
    }

    return "";
  }

  function getFiberName(fiber) {
    const type = fiber?.elementType || fiber?.type;
    return (
      type?.displayName ||
      type?.name ||
      fiber?._debugOwner?.elementType?.displayName ||
      fiber?._debugOwner?.elementType?.name ||
      fiber?._debugOwner?.type?.displayName ||
      fiber?._debugOwner?.type?.name ||
      ""
    );
  }

  function getVideoIdFromProps(props) {
    const id = props?.video?.id;
    return cleanNumericId(id);
  }

  function getVideoIdFromFiberProps(fiber) {
    return (
      getVideoIdFromProps(fiber?.memoizedProps) ||
      getVideoIdFromProps(fiber?.pendingProps)
    );
  }

  function isVideoControlsFiber(fiber) {
    const componentName = getFiberName(fiber);
    return componentName.includes(
      "FBUnifiedLightweightVideoAttachmentMediaControls"
    );
  }

  function getDevToolsHook() {
    return pageWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__ || null;
  }

  function getVideoBoundary(videoEle, container) {
    return (
      closestAncestor(
        videoEle,
        '[role="article"],[data-pagelet^="FeedUnit_"]'
      ) ||
      closestAncestor(
        container,
        '[role="article"],[data-pagelet^="FeedUnit_"]'
      ) ||
      container ||
      videoEle?.parentElement ||
      null
    );
  }

  function getDescendantHostElement(fiber) {
    const stack = [fiber?.child];
    const visited = new Set();
    let count = 0;

    while (stack.length && count < 300) {
      const current = stack.pop();
      if (!current || visited.has(current)) continue;

      visited.add(current);
      count++;

      if (current.stateNode?.nodeType === Node.ELEMENT_NODE) {
        return current.stateNode;
      }

      if (current.sibling) stack.push(current.sibling);
      if (current.child) stack.push(current.child);
    }

    return null;
  }

  function getAncestorHostElement(fiber) {
    let current = fiber;
    let level = 0;

    while (current && level < 80) {
      if (current.stateNode?.nodeType === Node.ELEMENT_NODE) {
        return current.stateNode;
      }

      current = current.return;
      level++;
    }

    return null;
  }

  function getFiberHostElement(fiber) {
    return getDescendantHostElement(fiber) || getAncestorHostElement(fiber);
  }

  function getRectDistance(a, b) {
    if (!a || !b || !a.width || !a.height || !b.width || !b.height) {
      return Number.POSITIVE_INFINITY;
    }

    const ax = a.left + a.width / 2;
    const ay = a.top + a.height / 2;
    const bx = b.left + b.width / 2;
    const by = b.top + b.height / 2;

    return Math.hypot(ax - bx, ay - by);
  }

  function getRectIntersectionRatio(a, b) {
    if (!a || !b || !a.width || !a.height || !b.width || !b.height) return 0;

    const left = Math.max(a.left, b.left);
    const top = Math.max(a.top, b.top);
    const right = Math.min(a.right, b.right);
    const bottom = Math.min(a.bottom, b.bottom);
    const width = Math.max(0, right - left);
    const height = Math.max(0, bottom - top);
    const intersection = width * height;
    const smallerArea = Math.min(a.width * a.height, b.width * b.height);

    return smallerArea ? intersection / smallerArea : 0;
  }

  function isHostElementRelatedToVideo(hostElement, videoEle, container) {
    if (!hostElement) return false;
    if (hostElement === videoEle) return true;
    if (hostElement.contains?.(videoEle) || videoEle?.contains?.(hostElement)) {
      return true;
    }
    if (
      container?.contains?.(hostElement) ||
      hostElement.contains?.(container)
    ) {
      return true;
    }

    const boundary = getVideoBoundary(videoEle, container);
    if (boundary?.contains?.(hostElement)) return true;

    const hostRect = hostElement.getBoundingClientRect?.();
    const videoRect = videoEle?.getBoundingClientRect?.();
    return getRectDistance(hostRect, videoRect) < 700;
  }

  function collectVideoIdCandidatesFromFiberSubtree(
    rootFiber,
    videoEle,
    container,
    limit = 60000
  ) {
    const stack = [rootFiber];
    const visited = new Set();
    const candidates = [];
    let count = 0;

    while (stack.length && count < limit) {
      const fiber = stack.pop();
      if (!fiber || visited.has(fiber)) continue;

      visited.add(fiber);
      count++;

      const videoId = getVideoIdFromFiberProps(fiber);
      if (videoId) {
        const hostElement = getFiberHostElement(fiber);
        const hostRect = hostElement?.getBoundingClientRect?.();
        const videoRect = videoEle?.getBoundingClientRect?.();
        const isRelated = isHostElementRelatedToVideo(
          hostElement,
          videoEle,
          container
        );
        const overlap = getRectIntersectionRatio(hostRect, videoRect);
        const distance = getRectDistance(hostRect, videoRect);

        candidates.push({
          id: videoId,
          isVideoControls: isVideoControlsFiber(fiber),
          isRelated,
          score:
            (isRelated ? 1000 : 0) +
            (isVideoControlsFiber(fiber) ? 500 : 0) +
            Math.round(overlap * 300) -
            (Number.isFinite(distance) ? Math.round(distance) : 10000),
        });
      }

      if (fiber.sibling) stack.push(fiber.sibling);
      if (fiber.child) stack.push(fiber.child);
    }

    return candidates;
  }

  function pickNearestVideoIdCandidate(candidates) {
    return candidates
      .filter((item) => item.isRelated)
      .sort((a, b) => b.score - a.score)[0];
  }

  function getVideoIdFromDevToolsRoots(videoEle, container) {
    const hook = getDevToolsHook();
    if (!hook?.renderers || !hook?.getFiberRoots) return "";

    const candidates = [];

    for (let rendererId of hook.renderers.keys()) {
      let roots = null;

      try {
        roots = hook.getFiberRoots(rendererId);
      } catch (e) {
        console.log("FB video downloader: React root lookup failed", e);
      }

      if (!roots) continue;

      for (let root of roots) {
        const rootFiber = root?.current || root;
        candidates.push(
          ...collectVideoIdCandidatesFromFiberSubtree(
            rootFiber,
            videoEle,
            container,
            60000
          )
        );
      }
    }

    return pickNearestVideoIdCandidate(candidates)?.id || "";
  }

  function getVideoIdFromReactProps(videoEle) {
    try {
      let key = "";
      for (let k in videoEle.parentElement) {
        if (k.startsWith("__reactProps")) {
          key = k;
          break;
        }
      }
      const props = videoEle.parentElement[key].children.props;
      return props.videoFBID || props.coreVideoPlayerMetaData?.videoFBID;
    } catch (e) {
      console.log("ERROR on get videoFBID: ", e);
      return null;
    }
  }

  function getVideoIdFromVideoElement(videoEle) {
    const container = getVideoScope(videoEle);
    const videoId =
      getVideoIdFromDevToolsRoots(videoEle, container) ||
      getVideoIdFromStorySaverDom(videoEle, container) ||
      getVideoIdFromReactProps(videoEle);

    if (!videoId) {
      const hook = getDevToolsHook();
      console.log("FB video downloader: video id resolver failed", {
        href: location.href,
        hasVideo: !!videoEle,
        hasContainer: !!container,
        hasDevToolsHook: !!hook,
        rendererCount: hook?.renderers?.size || 0,
        instanceKey: getClosestInstanceKeyElement(
          videoEle,
          container
        )?.getAttribute?.("data-instancekey"),
      });
    }

    return videoId;
  }
 
  async function getWatchingVideoId() {
    let allVideos = Array.from(document.querySelectorAll("video"));
    let result = [];
 
    for (let video of allVideos) {
      let videoId = getVideoIdFromVideoElement(video);
      if (videoId) {
        result.push({
          videoId,
          overlapScore: getOverlapScore(video),
          playing: !!(
            video.currentTime > 0 &&
            !video.paused &&
            !video.ended &&
            video.readyState > 2
          ),
        });
      }
    }
 
    // if there is playing video => return that
    let playingVideo = result.find((_) => _.playing);
    if (playingVideo) return [playingVideo.videoId];
 
    // else return all videos in-viewport
    return result
      .filter((_) => _.videoId && (_.overlapScore > 0 || _.playing))
      .sort((a, b) => b.overlapScore - a.overlapScore)
      .map((_) => _.videoId);
  }
 
  async function getVideoUrlFromVideoId(videoId) {
    let dtsg = await getDtsg();
    try {
      return await getLinkFbVideo2(videoId, dtsg);
    } catch (e) {
      return await getLinkFbVideo1(videoId, dtsg);
    }
  }
 
  async function getLinkFbVideo2(videoId, dtsg) {
    let res = await fetch(
      "https://www.facebook.com/video/video_data_async/?video_id=" + videoId,
      {
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        body: stringifyVariables({
          __a: "1",
          fb_dtsg: dtsg,
        }),
      }
    );
 
    let text = await res.text();
    text = text.replace("for (;;);", "");
    let json = JSON.parse(text);
 
    const { hd_src, hd_src_no_ratelimit, sd_src, sd_src_no_ratelimit } =
      json?.payload || {};
 
    return hd_src_no_ratelimit || hd_src || sd_src_no_ratelimit || sd_src;
  }
 
  async function getLinkFbVideo1(videoId, dtsg) {
    let res = await fetchGraphQl("5279476072161634", {
      UFI2CommentsProvider_commentsKey: "CometTahoeSidePaneQuery",
      caller: "CHANNEL_VIEW_FROM_PAGE_TIMELINE",
      displayCommentsContextEnableComment: null,
      displayCommentsContextIsAdPreview: null,
      displayCommentsContextIsAggregatedShare: null,
      displayCommentsContextIsStorySet: null,
      displayCommentsFeedbackContext: null,
      feedbackSource: 41,
      feedLocation: "TAHOE",
      focusCommentID: null,
      privacySelectorRenderLocation: "COMET_STREAM",
      renderLocation: "video_channel",
      scale: 1,
      streamChainingSection: !1,
      useDefaultActor: !1,
      videoChainingContext: null,
      videoID: videoId,
    }, dtsg);
    let text = await res.text();
 
    let a = JSON.parse(text.split("\n")[0]),
      link = a.data.video.playable_url_quality_hd || a.data.video.playable_url;
 
    return link;
  }
 
  function fetchGraphQl(doc_id, variables, dtsg) {
    return fetch("https://www.facebook.com/api/graphql/", {
      method: "POST",
      headers: {
        "content-type": "application/x-www-form-urlencoded",
      },
      body: stringifyVariables({
        doc_id: doc_id,
        variables: JSON.stringify(variables),
        fb_dtsg: dtsg,
        server_timestamps: !0,
      }),
    });
  }
 
  function stringifyVariables(d, e) {
    let f = [],
      a;
    for (a in d)
      if (d.hasOwnProperty(a)) {
        let g = e ? e + "[" + a + "]" : a,
          b = d[a];
        f.push(
          null !== b && "object" == typeof b
            ? stringifyVariables(b, g)
            : encodeURIComponent(g) + "=" + encodeURIComponent(b)
        );
      }
    return f.join("&");
  }
 
  async function getDtsg() {
    return require("DTSGInitialData").token;
  }
 
  function downloadURL(url, name) {
    var link = document.createElement("a");
    link.target = "_blank";
    link.download = name;
    link.href = url;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  }
 
  async function downloadWatchingVideo() {
    try {
      let listVideoId = await getWatchingVideoId();
      if (!listVideoId?.length > 0) throw Error("No video found in the page");
 
      console.log(listVideoId)
 
      for (let videoId of listVideoId) {
        let videoUrl = await getVideoUrlFromVideoId(videoId);
        if (videoUrl) downloadURL(videoUrl, "fb_video.mp4");
      }
    } catch (e) {
      alert("ERROR: " + e);
    }
  }
 
  function resisterMenuCommand() {
    GM_registerMenuCommand("Download watching video", downloadWatchingVideo);
    GM_registerMenuCommand("Install FB AIO", () => window.open('https://fbaio.org'));
  }
 
  resisterMenuCommand();
})();