Greasy Fork is available in English.

Make YouTube Think Firefox is Chrome

Speeds up Youtube on Firefox by making it appear as Chrome by spoofing User-Agent, UA Client Hints, and window.chrome fingerprints.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
  1. // ==UserScript==
  2. // @name Make YouTube Think Firefox is Chrome
  3. // @namespace https://greasyfork.org/users/KosherKale
  4. // @version 1.2
  5. // @description Speeds up Youtube on Firefox by making it appear as Chrome by spoofing User-Agent, UA Client Hints, and window.chrome fingerprints.
  6. // @author kosherkale
  7. // @match https://www.youtube.com/*
  8. // @run-at document-start
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // --- Auto-updating Chrome version via localStorage cache ---
  17. // The cache is checked synchronously at document-start (no async delay).
  18. // Background fetch refreshes it every 7 days so it stays current automatically.
  19. const VERSION_CACHE_KEY = "yt_spoof_chrome_ver";
  20. const VERSION_CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
  21. const VERSION_FALLBACK = { major: "133", full: "133.0.6943.126" };
  22.  
  23. function getCachedVersion() {
  24. try {
  25. const cached = JSON.parse(localStorage.getItem(VERSION_CACHE_KEY));
  26. if (!cached?.ver) return null;
  27. // Always use cached value; trigger background refresh if expired
  28. if ((Date.now() - cached.ts) >= VERSION_CACHE_TTL) refreshVersionCache();
  29. // Migrate old cache format (plain string major version)
  30. if (typeof cached.ver === 'string') return { major: cached.ver, full: `${cached.ver}.0.0.0` };
  31. if (cached.ver?.major && cached.ver?.full) return cached.ver;
  32. } catch (_) {}
  33. return null;
  34. }
  35.  
  36. function setCachedVersion(ver) {
  37. try {
  38. localStorage.setItem(VERSION_CACHE_KEY, JSON.stringify({ ver, ts: Date.now() }));
  39. } catch (_) {}
  40. }
  41.  
  42. // Happens in background, result will take effect on next page load
  43. async function refreshVersionCache() {
  44. try {
  45. const res = await fetch(
  46. "https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions" +
  47. "?filter=endtime=none&order_by=version%20desc&page_size=1"
  48. );
  49. const data = await res.json();
  50. if (!data.versions?.length) return;
  51. const full = data.versions[0].version; // e.g. "133.0.6943.126"
  52. const major = full.split(".")[0]; // e.g. "133"
  53. setCachedVersion({ major, full });
  54. } catch (_) {}
  55. }
  56.  
  57. // Sync and refresh cache in background
  58. const cached = getCachedVersion() || VERSION_FALLBACK;
  59. const CHROME_VERSION = cached.major;
  60. const CHROME_FULL_VERSION = cached.full;
  61. refreshVersionCache(); // async, non-blocking
  62.  
  63. // --- Spoofing ---
  64.  
  65. // Use full version in UA string (e.g. Chrome/133.0.6943.126) to match real Chrome
  66. const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROME_FULL_VERSION} Safari/537.36`;
  67.  
  68. function override(obj, prop, value) {
  69. Object.defineProperty(obj, prop, {
  70. get: () => value,
  71. configurable: true,
  72. enumerable: true
  73. });
  74. }
  75.  
  76. // navigator.deviceMemory (Device Memory API)
  77. // Chrome exposes this; Firefox doesn't. YouTube uses it to size internal JS caches
  78. // and pre-fetch budgets. Without it, YouTube defaults to the minimum memory budget.
  79. // Value is rounded to the nearest power of 2 (1/0.25/0.5/1/2/4/8); 8 = 8GB+.
  80. if (!('deviceMemory' in navigator)) {
  81. override(Navigator.prototype, "deviceMemory", 8);
  82. }
  83.  
  84. // navigator.scheduling.isInputPending (Input Pending API)
  85. // Chrome exposes this for cooperative scheduling — lets code yield before a pending
  86. // user input event. YouTube uses it during page init to avoid blocking the main thread.
  87. if (!navigator.scheduling?.isInputPending) {
  88. const scheduling = navigator.scheduling || {};
  89. scheduling.isInputPending = function() { return false; };
  90. if (!navigator.scheduling) {
  91. override(Navigator.prototype, "scheduling", scheduling);
  92. }
  93. }
  94.  
  95. // Basic navigator properties
  96. override(Navigator.prototype, "userAgent", chromeUA);
  97. override(Navigator.prototype, "appVersion", chromeUA.slice("Mozilla/".length));
  98. override(Navigator.prototype, "platform", "Win32");
  99. override(Navigator.prototype, "vendor", "Google Inc.");
  100. override(Navigator.prototype, "oscpu", undefined); // Chrome doesn't expose this; Firefox does
  101. override(Navigator.prototype, "productSub", "20030107"); // Chrome: "20030107", Firefox: "20100101"
  102.  
  103. // User-Agent Client Hints
  104. // GREASE brand string and version rotate based on Chrome major version to prevent fingerprinting
  105. function getGreasedBrand(major) {
  106. const n = parseInt(major, 10) % 3;
  107. const brandStrings = ["Not A(Brand", "Not)A;Brand", "Not:A-Brand"];
  108. const brandVersions = ["8", "99", "24"];
  109. return { brand: brandStrings[n], version: brandVersions[n] };
  110. }
  111.  
  112. const greaseBrand = getGreasedBrand(CHROME_VERSION);
  113. const greaseBrandFull = { brand: greaseBrand.brand, version: `${greaseBrand.version}.0.0.0` };
  114.  
  115. const brands = [
  116. greaseBrand,
  117. { brand: "Google Chrome", version: CHROME_VERSION },
  118. { brand: "Chromium", version: CHROME_VERSION }
  119. ];
  120. const fullVersionList = [
  121. greaseBrandFull,
  122. { brand: "Google Chrome", version: CHROME_FULL_VERSION },
  123. { brand: "Chromium", version: CHROME_FULL_VERSION }
  124. ];
  125.  
  126. // platformVersion: "15.0.0" = Windows 11 22H2+ (matches majority of active Chrome installs)
  127. // To use Windows 10 instead: localStorage.setItem('yt_spoof_winver', '10.0.0')
  128. const platformVersion = localStorage.getItem('yt_spoof_winver') || "15.0.0";
  129.  
  130. // Built once, reused on every getHighEntropyValues() call
  131. const highEntropyMap = {
  132. architecture: "x86",
  133. bitness: "64",
  134. brands,
  135. fullVersionList,
  136. mobile: false,
  137. model: "",
  138. platform: "Windows",
  139. platformVersion,
  140. uaFullVersion: CHROME_FULL_VERSION,
  141. };
  142.  
  143. const uaData = {
  144. brands,
  145. mobile: false,
  146. platform: "Windows",
  147. getHighEntropyValues: async function(hints) {
  148. const result = {};
  149. for (const hint of hints) {
  150. if (hint in highEntropyMap) result[hint] = highEntropyMap[hint];
  151. }
  152. return result;
  153. },
  154. toJSON: function() {
  155. return { brands, mobile: false, platform: "Windows" };
  156. }
  157. };
  158. override(Navigator.prototype, "userAgentData", uaData);
  159.  
  160. // window.chrome
  161. // Always assign rather than guard with if(!window.chrome) — newer Firefox versions
  162. // pre-define a partial window.chrome for extension APIs, which causes YouTube to see
  163. // an incomplete object and fall back to non-Chrome behavior (including skipping autoplay).
  164. const chromeMethods = {
  165. app: {
  166. isInstalled: false,
  167. InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
  168. RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' }
  169. },
  170. runtime: {
  171. OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },
  172. OnRestartRequiredReason: { APP_UPDATE: 'app_update', GC_PRESSURE: 'gc_pressure', OS_UPDATE: 'os_update' },
  173. PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
  174. PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
  175. RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' }
  176. },
  177. // Stub webstore — YouTube checks for its presence as part of Chrome detection
  178. webstore: {
  179. onInstallStageChanged: {},
  180. onDownloadProgress: {}
  181. },
  182. loadTimes: function() {
  183. const nav = performance.getEntriesByType('navigation')[0];
  184. const o = performance.timeOrigin / 1000;
  185. const t = (ms) => ms ? o + ms / 1000 : o;
  186. return {
  187. requestTime: t(nav?.fetchStart),
  188. startLoadTime: t(nav?.startTime),
  189. commitLoadTime: t(nav?.responseEnd),
  190. finishDocumentLoadTime: t(nav?.domContentLoadedEventEnd),
  191. finishLoadTime: t(nav?.loadEventEnd),
  192. firstPaintTime: 0,
  193. firstPaintAfterLoadTime: 0,
  194. navigationType: nav?.type === 'reload' ? 'Reload'
  195. : nav?.type === 'back_forward' ? 'BackForward'
  196. : 'Other',
  197. wasFetchedViaSpdy: false,
  198. wasNpnNegotiated: true,
  199. npnNegotiatedProtocol: "h3",
  200. wasAlternateProtocolAvailable: false,
  201. connectionInfo: "h3"
  202. };
  203. },
  204. csi: function() {
  205. return { startE: Date.now(), onloadT: Date.now(), pageT: 0, tran: 15 };
  206. }
  207. };
  208.  
  209. // Merge into window.chrome so any existing Firefox extension properties are preserved
  210. window.chrome = Object.assign(window.chrome || {}, chromeMethods);
  211.  
  212. // --- Autoplay fix ---
  213. // YouTube queries navigator.permissions for 'autoplay' before attempting playback.
  214. // Firefox returns 'prompt' or throws, causing YouTube to wait for manual play.
  215. // Intercept and return 'granted' for autoplay queries only.
  216. if (navigator.permissions?.query) {
  217. const _query = navigator.permissions.query.bind(navigator.permissions);
  218. Object.defineProperty(navigator.permissions, 'query', {
  219. value: function(desc) {
  220. if (desc?.name === 'autoplay') {
  221. return Promise.resolve({ state: 'granted', onchange: null });
  222. }
  223. return _query(desc);
  224. },
  225. configurable: true,
  226. writable: true
  227. });
  228. }
  229.  
  230. // --- Performance fixes ---
  231.  
  232. // navigator.connection (Network Information API)
  233. // YouTube uses this to set initial buffer size, quality ceiling, and ABR aggressiveness.
  234. // Firefox doesn't expose it; without it YouTube defaults to slow/conservative buffering.
  235. if (!navigator.connection) {
  236. override(Navigator.prototype, "connection", {
  237. effectiveType: "4g",
  238. downlink: 20,
  239. downlinkMax: Infinity,
  240. rtt: 25,
  241. saveData: false,
  242. type: "wifi",
  243. onchange: null,
  244. ontypechange: null,
  245. addEventListener: () => {},
  246. removeEventListener: () => {},
  247. dispatchEvent: () => false
  248. });
  249. }
  250.  
  251. // navigator.mediaCapabilities
  252. // YouTube calls decodingInfo() to decide which codec/resolution to serve.
  253. // Reporting smooth+powerEfficient for all configs ensures it picks the best stream
  254. // rather than falling back to H.264 (larger files, slower load for same quality).
  255. if (navigator.mediaCapabilities) {
  256. const _decodingInfo = navigator.mediaCapabilities.decodingInfo.bind(navigator.mediaCapabilities);
  257. navigator.mediaCapabilities.decodingInfo = async function(config) {
  258. try {
  259. const result = await _decodingInfo(config);
  260. // Ensure Firefox never reports a codec as non-smooth to YouTube
  261. return { supported: result.supported, smooth: true, powerEfficient: true };
  262. } catch (_) {
  263. return { supported: true, smooth: true, powerEfficient: true };
  264. }
  265. };
  266. }
  267.  
  268. // scheduler.postTask (Prioritized Task Scheduling API)
  269. // Chrome's task scheduler used by YouTube's player internals for prioritized work.
  270. // Without it, YouTube falls back to unordered setTimeout chains, slowing player init.
  271. if (!window.scheduler) {
  272. window.scheduler = {
  273. postTask: function(callback, options) {
  274. const delay = options?.delay ?? 0;
  275. return new Promise((resolve, reject) => {
  276. setTimeout(() => {
  277. try { resolve(callback()); } catch (e) { reject(e); }
  278. }, delay);
  279. });
  280. },
  281. // scheduler.yield — Chrome 124+; yields control back to the event loop then resumes.
  282. // YouTube uses this inside the player init path for responsive scheduling.
  283. yield: function() {
  284. return new Promise(resolve => setTimeout(resolve, 0));
  285. }
  286. };
  287. } else if (!window.scheduler.yield) {
  288. // scheduler exists (e.g. partial Firefox impl) but yield is missing
  289. window.scheduler.yield = function() {
  290. return new Promise(resolve => setTimeout(resolve, 0));
  291. };
  292. }
  293.  
  294. // document.startViewTransition (View Transitions API)
  295. // YouTube's SPA uses this for smooth navigation between pages/videos.
  296. // Without it, the player tears down and reinitializes on every navigation
  297. // instead of transitioning, causing a visible slow reload between videos.
  298. if (!document.startViewTransition) {
  299. document.startViewTransition = function(callback) {
  300. const p = Promise.resolve().then(() => callback?.());
  301. return { ready: p, finished: p, updateCallbackDone: p, skipTransition: () => {} };
  302. };
  303. }
  304.  
  305. })();