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.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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); // старт
})();