Twitch Max Quality

Always forces Twitch to the highest available quality (Source / 1080p / 1440p) and instantly restores it after ads, channel switches, player reloads or tab switches. No need to touch the quality menu.

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         Twitch Max Quality
// @name:en      Twitch Max Quality
// @name:ru      Twitch Max Quality: всегда максимальное качество (1080p/1440p)
// @namespace    https://greasyfork.org/scripts/twitch-force-max-quality
// @version      16.2
// @description  Always forces Twitch to the highest available quality (Source / 1080p / 1440p) and instantly restores it after ads, channel switches, player reloads or tab switches. No need to touch the quality menu.
// @description:en  Always forces Twitch to the highest available quality (Source / 1080p / 1440p) and instantly restores it after ads, channel switches, player reloads or tab switches. No need to touch the quality menu.
// @description:ru  Всегда ставит максимальное качество Twitch (Source / 1080p / 1440p) и мгновенно возвращает его после рекламы, смены канала, перезагрузки плеера или переключения вкладки. Без ручного выбора в меню.
// @author       vector010
// @match        *://*.twitch.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitch.tv
// @run-at       document-start
// @noframes
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
	'use strict';

	// Поставь 1080, если нет H.265/HEVC и 1440p даёт чёрный экран.
	const MAX_HEIGHT = Infinity;

	function getFiber(el) {
		if (!el) return null;
		for (const k in el) {
			if (k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')) return el[k];
		}
		return null;
	}

	function findPlayer() {
		const sels = ['video', '[data-a-target="video-player"]', '.video-player', 'div[data-a-player-state]'];
		let fiber = null;
		for (const s of sels) { fiber = getFiber(document.querySelector(s)); if (fiber) break; }
		if (!fiber) return null;
		const seen = new Set();
		const stack = [fiber];
		let guard = 0;
		while (stack.length && guard++ < 30000) {
			const node = stack.pop();
			if (!node || seen.has(node)) continue;
			seen.add(node);
			for (const p of [node.memoizedProps, node.pendingProps, node.stateNode]) {
				if (!p) continue;
				const cand = p.mediaPlayerInstance || p.player ||
					((typeof p.setQuality === 'function' || typeof p.getQualities === 'function') ? p : null);
				if (cand && typeof cand.getQualities === 'function' && typeof cand.setQuality === 'function') return cand;
			}
			if (node.child) stack.push(node.child);
			if (node.sibling) stack.push(node.sibling);
			if (node.return) stack.push(node.return);
		}
		return null;
	}

	function getQualities(p) {
		try { return p.getQualities() || []; } catch (e) {}
		try { return (p.core && p.core.getQualities()) || []; } catch (e) {}
		return [];
	}

	function currentName(p) {
		try {
			const q = p.getQuality && p.getQuality();
			return q && (q.name || q.group || (typeof q === 'string' ? q : null));
		} catch (e) { return null; }
	}

	// ВАЖНО: новый плеер Twitch (Amazon IVS) ждёт ОБЪЕКТ качества, а не строку.
	function applyQuality(p, q) {
		try { if (typeof p.setAutoQualityMode === 'function') p.setAutoQualityMode(false); } catch (e) {}
		try { p.setQuality(q, false); return true; } catch (e) {}   // IVS: объект + adaptive=false
		try { p.setQuality(q); return true; } catch (e) {}          // IVS: объект
		try { p.setQuality(q.group); return true; } catch (e) {}    // старый API: строка группы
		return false;
	}

	function pickMax() {
		const p = findPlayer();
		if (!p) return false;
		const qs = getQualities(p);
		if (!qs.length) return false;
		let real = qs.filter(q => q && String(q.name || '').toLowerCase() !== 'auto' && q.group !== 'auto');
		if (isFinite(MAX_HEIGHT)) real = real.filter(q => (q.height || 0) <= MAX_HEIGHT);
		if (!real.length) return false;
		const best = real.slice().sort((a, b) =>
			((b.height || 0) - (a.height || 0)) || ((b.bitrate || 0) - (a.bitrate || 0)))[0];
		if (!best) return false;
		const cur = currentName(p);
		if (cur && best.name && cur === best.name) return true; // уже на максимуме
		return applyQuality(p, best);
	}

	// мягкие повторы: останавливаемся при успехе, не спамим
	let timer = null, until = 0, okCount = 0;
	function burst(durationMs = 10000, stepMs = 400) {
		until = Math.max(until, Date.now() + durationMs);
		okCount = 0;
		if (timer) return;
		timer = setInterval(() => {
			let done = false;
			try { done = pickMax(); } catch (e) {}
			if (done) { okCount++; if (okCount >= 2) { clearInterval(timer); timer = null; return; } }
			if (Date.now() > until) { clearInterval(timer); timer = null; }
		}, stepMs);
	}

	function onNav() { burst(10000, 400); }
	const _push = history.pushState;
	history.pushState = function () { const r = _push.apply(this, arguments); dispatchEvent(new Event('tmq:nav')); return r; };
	const _rep = history.replaceState;
	history.replaceState = function () { const r = _rep.apply(this, arguments); dispatchEvent(new Event('tmq:nav')); return r; };
	addEventListener('popstate', onNav);
	addEventListener('tmq:nav', onNav);

	let lastVideo = null, scheduled = false;
	new MutationObserver(() => {
		if (scheduled) return;
		scheduled = true;
		requestAnimationFrame(() => {
			scheduled = false;
			const v = document.querySelector('video');
			if (v && v !== lastVideo) { lastVideo = v; burst(10000, 400); }
		});
	}).observe(document.documentElement, { childList: true, subtree: true });

	addEventListener('load', () => burst(10000, 400));
	addEventListener('visibilitychange', () => { if (!document.hidden) burst(4000, 400); });

	burst(12000, 400); // старт
})();