Download any video on Facebook (post/chat/comment)
// ==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();
})();