Greasy Fork is available in English.
Speeds up Youtube on Firefox by making it appear as Chrome by spoofing User-Agent, UA Client Hints, and window.chrome fingerprints.
- // ==UserScript==
- // @name Make YouTube Think Firefox is Chrome
- // @namespace https://greasyfork.org/users/KosherKale
- // @version 1.2
- // @description Speeds up Youtube on Firefox by making it appear as Chrome by spoofing User-Agent, UA Client Hints, and window.chrome fingerprints.
- // @author kosherkale
- // @match https://www.youtube.com/*
- // @run-at document-start
- // @grant none
- // @license MIT
- // ==/UserScript==
- (function() {
- 'use strict';
- // --- Auto-updating Chrome version via localStorage cache ---
- // The cache is checked synchronously at document-start (no async delay).
- // Background fetch refreshes it every 7 days so it stays current automatically.
- const VERSION_CACHE_KEY = "yt_spoof_chrome_ver";
- const VERSION_CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
- const VERSION_FALLBACK = { major: "133", full: "133.0.6943.126" };
- function getCachedVersion() {
- try {
- const cached = JSON.parse(localStorage.getItem(VERSION_CACHE_KEY));
- if (!cached?.ver) return null;
- // Always use cached value; trigger background refresh if expired
- if ((Date.now() - cached.ts) >= VERSION_CACHE_TTL) refreshVersionCache();
- // Migrate old cache format (plain string major version)
- if (typeof cached.ver === 'string') return { major: cached.ver, full: `${cached.ver}.0.0.0` };
- if (cached.ver?.major && cached.ver?.full) return cached.ver;
- } catch (_) {}
- return null;
- }
- function setCachedVersion(ver) {
- try {
- localStorage.setItem(VERSION_CACHE_KEY, JSON.stringify({ ver, ts: Date.now() }));
- } catch (_) {}
- }
- // Happens in background, result will take effect on next page load
- async function refreshVersionCache() {
- try {
- const res = await fetch(
- "https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions" +
- "?filter=endtime=none&order_by=version%20desc&page_size=1"
- );
- const data = await res.json();
- if (!data.versions?.length) return;
- const full = data.versions[0].version; // e.g. "133.0.6943.126"
- const major = full.split(".")[0]; // e.g. "133"
- setCachedVersion({ major, full });
- } catch (_) {}
- }
- // Sync and refresh cache in background
- const cached = getCachedVersion() || VERSION_FALLBACK;
- const CHROME_VERSION = cached.major;
- const CHROME_FULL_VERSION = cached.full;
- refreshVersionCache(); // async, non-blocking
- // --- Spoofing ---
- // Use full version in UA string (e.g. Chrome/133.0.6943.126) to match real Chrome
- const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROME_FULL_VERSION} Safari/537.36`;
- function override(obj, prop, value) {
- Object.defineProperty(obj, prop, {
- get: () => value,
- configurable: true,
- enumerable: true
- });
- }
- // navigator.deviceMemory (Device Memory API)
- // Chrome exposes this; Firefox doesn't. YouTube uses it to size internal JS caches
- // and pre-fetch budgets. Without it, YouTube defaults to the minimum memory budget.
- // Value is rounded to the nearest power of 2 (1/0.25/0.5/1/2/4/8); 8 = 8GB+.
- if (!('deviceMemory' in navigator)) {
- override(Navigator.prototype, "deviceMemory", 8);
- }
- // navigator.scheduling.isInputPending (Input Pending API)
- // Chrome exposes this for cooperative scheduling — lets code yield before a pending
- // user input event. YouTube uses it during page init to avoid blocking the main thread.
- if (!navigator.scheduling?.isInputPending) {
- const scheduling = navigator.scheduling || {};
- scheduling.isInputPending = function() { return false; };
- if (!navigator.scheduling) {
- override(Navigator.prototype, "scheduling", scheduling);
- }
- }
- // Basic navigator properties
- override(Navigator.prototype, "userAgent", chromeUA);
- override(Navigator.prototype, "appVersion", chromeUA.slice("Mozilla/".length));
- override(Navigator.prototype, "platform", "Win32");
- override(Navigator.prototype, "vendor", "Google Inc.");
- override(Navigator.prototype, "oscpu", undefined); // Chrome doesn't expose this; Firefox does
- override(Navigator.prototype, "productSub", "20030107"); // Chrome: "20030107", Firefox: "20100101"
- // User-Agent Client Hints
- // GREASE brand string and version rotate based on Chrome major version to prevent fingerprinting
- function getGreasedBrand(major) {
- const n = parseInt(major, 10) % 3;
- const brandStrings = ["Not A(Brand", "Not)A;Brand", "Not:A-Brand"];
- const brandVersions = ["8", "99", "24"];
- return { brand: brandStrings[n], version: brandVersions[n] };
- }
- const greaseBrand = getGreasedBrand(CHROME_VERSION);
- const greaseBrandFull = { brand: greaseBrand.brand, version: `${greaseBrand.version}.0.0.0` };
- const brands = [
- greaseBrand,
- { brand: "Google Chrome", version: CHROME_VERSION },
- { brand: "Chromium", version: CHROME_VERSION }
- ];
- const fullVersionList = [
- greaseBrandFull,
- { brand: "Google Chrome", version: CHROME_FULL_VERSION },
- { brand: "Chromium", version: CHROME_FULL_VERSION }
- ];
- // platformVersion: "15.0.0" = Windows 11 22H2+ (matches majority of active Chrome installs)
- // To use Windows 10 instead: localStorage.setItem('yt_spoof_winver', '10.0.0')
- const platformVersion = localStorage.getItem('yt_spoof_winver') || "15.0.0";
- // Built once, reused on every getHighEntropyValues() call
- const highEntropyMap = {
- architecture: "x86",
- bitness: "64",
- brands,
- fullVersionList,
- mobile: false,
- model: "",
- platform: "Windows",
- platformVersion,
- uaFullVersion: CHROME_FULL_VERSION,
- };
- const uaData = {
- brands,
- mobile: false,
- platform: "Windows",
- getHighEntropyValues: async function(hints) {
- const result = {};
- for (const hint of hints) {
- if (hint in highEntropyMap) result[hint] = highEntropyMap[hint];
- }
- return result;
- },
- toJSON: function() {
- return { brands, mobile: false, platform: "Windows" };
- }
- };
- override(Navigator.prototype, "userAgentData", uaData);
- // window.chrome
- // Always assign rather than guard with if(!window.chrome) — newer Firefox versions
- // pre-define a partial window.chrome for extension APIs, which causes YouTube to see
- // an incomplete object and fall back to non-Chrome behavior (including skipping autoplay).
- const chromeMethods = {
- app: {
- isInstalled: false,
- InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
- RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' }
- },
- runtime: {
- OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },
- OnRestartRequiredReason: { APP_UPDATE: 'app_update', GC_PRESSURE: 'gc_pressure', OS_UPDATE: 'os_update' },
- PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
- PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
- RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' }
- },
- // Stub webstore — YouTube checks for its presence as part of Chrome detection
- webstore: {
- onInstallStageChanged: {},
- onDownloadProgress: {}
- },
- loadTimes: function() {
- const nav = performance.getEntriesByType('navigation')[0];
- const o = performance.timeOrigin / 1000;
- const t = (ms) => ms ? o + ms / 1000 : o;
- return {
- requestTime: t(nav?.fetchStart),
- startLoadTime: t(nav?.startTime),
- commitLoadTime: t(nav?.responseEnd),
- finishDocumentLoadTime: t(nav?.domContentLoadedEventEnd),
- finishLoadTime: t(nav?.loadEventEnd),
- firstPaintTime: 0,
- firstPaintAfterLoadTime: 0,
- navigationType: nav?.type === 'reload' ? 'Reload'
- : nav?.type === 'back_forward' ? 'BackForward'
- : 'Other',
- wasFetchedViaSpdy: false,
- wasNpnNegotiated: true,
- npnNegotiatedProtocol: "h3",
- wasAlternateProtocolAvailable: false,
- connectionInfo: "h3"
- };
- },
- csi: function() {
- return { startE: Date.now(), onloadT: Date.now(), pageT: 0, tran: 15 };
- }
- };
- // Merge into window.chrome so any existing Firefox extension properties are preserved
- window.chrome = Object.assign(window.chrome || {}, chromeMethods);
- // --- Autoplay fix ---
- // YouTube queries navigator.permissions for 'autoplay' before attempting playback.
- // Firefox returns 'prompt' or throws, causing YouTube to wait for manual play.
- // Intercept and return 'granted' for autoplay queries only.
- if (navigator.permissions?.query) {
- const _query = navigator.permissions.query.bind(navigator.permissions);
- Object.defineProperty(navigator.permissions, 'query', {
- value: function(desc) {
- if (desc?.name === 'autoplay') {
- return Promise.resolve({ state: 'granted', onchange: null });
- }
- return _query(desc);
- },
- configurable: true,
- writable: true
- });
- }
- // --- Performance fixes ---
- // navigator.connection (Network Information API)
- // YouTube uses this to set initial buffer size, quality ceiling, and ABR aggressiveness.
- // Firefox doesn't expose it; without it YouTube defaults to slow/conservative buffering.
- if (!navigator.connection) {
- override(Navigator.prototype, "connection", {
- effectiveType: "4g",
- downlink: 20,
- downlinkMax: Infinity,
- rtt: 25,
- saveData: false,
- type: "wifi",
- onchange: null,
- ontypechange: null,
- addEventListener: () => {},
- removeEventListener: () => {},
- dispatchEvent: () => false
- });
- }
- // navigator.mediaCapabilities
- // YouTube calls decodingInfo() to decide which codec/resolution to serve.
- // Reporting smooth+powerEfficient for all configs ensures it picks the best stream
- // rather than falling back to H.264 (larger files, slower load for same quality).
- if (navigator.mediaCapabilities) {
- const _decodingInfo = navigator.mediaCapabilities.decodingInfo.bind(navigator.mediaCapabilities);
- navigator.mediaCapabilities.decodingInfo = async function(config) {
- try {
- const result = await _decodingInfo(config);
- // Ensure Firefox never reports a codec as non-smooth to YouTube
- return { supported: result.supported, smooth: true, powerEfficient: true };
- } catch (_) {
- return { supported: true, smooth: true, powerEfficient: true };
- }
- };
- }
- // scheduler.postTask (Prioritized Task Scheduling API)
- // Chrome's task scheduler used by YouTube's player internals for prioritized work.
- // Without it, YouTube falls back to unordered setTimeout chains, slowing player init.
- if (!window.scheduler) {
- window.scheduler = {
- postTask: function(callback, options) {
- const delay = options?.delay ?? 0;
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- try { resolve(callback()); } catch (e) { reject(e); }
- }, delay);
- });
- },
- // scheduler.yield — Chrome 124+; yields control back to the event loop then resumes.
- // YouTube uses this inside the player init path for responsive scheduling.
- yield: function() {
- return new Promise(resolve => setTimeout(resolve, 0));
- }
- };
- } else if (!window.scheduler.yield) {
- // scheduler exists (e.g. partial Firefox impl) but yield is missing
- window.scheduler.yield = function() {
- return new Promise(resolve => setTimeout(resolve, 0));
- };
- }
- // document.startViewTransition (View Transitions API)
- // YouTube's SPA uses this for smooth navigation between pages/videos.
- // Without it, the player tears down and reinitializes on every navigation
- // instead of transitioning, causing a visible slow reload between videos.
- if (!document.startViewTransition) {
- document.startViewTransition = function(callback) {
- const p = Promise.resolve().then(() => callback?.());
- return { ready: p, finished: p, updateCallbackDone: p, skipTransition: () => {} };
- };
- }
- })();